Skip to content

Commit

Permalink
Merge pull request #22 from broadinstitute/cjl_go_to_declaration
Browse files Browse the repository at this point in the history
Reference resolution from values to declarations.
  • Loading branch information
cjllanwarne committed Oct 17, 2017
2 parents 78a6592 + a489961 commit c9e3913
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 65 deletions.
11 changes: 8 additions & 3 deletions README.md
Expand Up @@ -2,8 +2,13 @@

This plug-in currently supports:
* Syntax highlighting
* Collapsible code blocks for workflows, tasks, and more.
* Highlighting and completion of parentheses `()` and other braces.
* Allows auto-commenting of lines in WDL with <kbd>CMD</KBD>+<kbd>/</KBD>
* Undeclared value detection
* "Go to declaration" (currently for declared values only)

Winstanley is open sourced under the BSD 3-Clause license.
More features will be coming soon!

## Getting Started

Expand All @@ -24,8 +29,8 @@ To build or test the plugin using IntelliJ:
4. Make sure the repo has a valid Scala SDK attached as a project dependency.
* Otherwise you'll see errors like `"Cannot find class WdlElementType"` even though it's clearly there!
5. Generate the necessary files (on Mac):
* Navigate to Wdl.flex and generate sources using \[Command + Shift + G\]
* Navigate to wdl.bnf and generate sources using \[Command + Shift + G\]
* Navigate to Wdl.flex and generate sources using <kbd>CMD</KBD>+<kbd>SHIFT</KBD>+<kbd>G</KBD>.
* Navigate to wdl.bnf and generate sources using <kbd>CMD</KBD>+<kbd>SHIFT</KBD>+<kbd>G</KBD>
6. At this point, you can run or test the project using IntelliJ's preset run modes.
* Open the `Run Configurations` window (for me, in the top right of the IntelliJ window)
* Add a new one configuration with the `+` icon.
Expand Down
19 changes: 6 additions & 13 deletions src/winstanley/WdlAnnotator.scala
Expand Up @@ -2,27 +2,20 @@ package winstanley

import com.intellij.lang.annotation.{AnnotationHolder, Annotator}
import com.intellij.psi.PsiElement
import winstanley.psi.WdlValue
import winstanley.psi.WdlVariableLookup
import winstanley.structure.WdlImplicits._


class WdlAnnotator extends Annotator {
override def annotate(psiElement: PsiElement, annotationHolder: AnnotationHolder): Unit = psiElement match {
case value: WdlValue =>
case value: WdlVariableLookup =>

// If this value is an identifier, make sure that it's been declared somewhere (either in a declaration or in a scatter)
value.asIdentifierNode foreach { identifier =>
val declarationNames = value.findDeclarationsAvailableInScope.map(_.declaredValueName) collect { case Some(d) => d }

val scatterVariableName = for {
outerscatter <- value.findContainingScatter
scatterVariable <- outerscatter.getIdentifierNode
} yield scatterVariable.getText

val availableValueNames = declarationNames ++ scatterVariableName

value.getIdentifierNode foreach { identifier =>
val identifierText = identifier.getText
if (!availableValueNames.contains(identifierText)) {
val declarationNames = value.findDeclarationsAvailableInScope.flatMap(_.declaredValueName)

if (!declarationNames.contains(identifierText)) {
annotationHolder.createErrorAnnotation(identifier.getTextRange, s"No declaration found for '${identifier.getText}'")
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/winstanley/psi/WdlNamedElement.scala
@@ -0,0 +1,7 @@
package winstanley.psi

import com.intellij.psi.PsiNameIdentifierOwner

trait WdlNamedElement extends PsiNameIdentifierOwner {
def declaredValueName: Option[String]
}
11 changes: 11 additions & 0 deletions src/winstanley/psi/impl/WdlNamedElementImpl.scala
@@ -0,0 +1,11 @@
package winstanley.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import winstanley.structure.WdlImplicits._
import winstanley.psi.WdlNamedElement

abstract class WdlNamedElementImpl(astNode: ASTNode) extends ASTWrapperPsiElement(astNode) with WdlNamedElement {
// The Option-ality is a little paranoid, but if the declaration is being edited it might be temporarily nameless:
override def declaredValueName: Option[String] = astNode.getPsi.getIdentifierNode.map(_.getText)
}
34 changes: 34 additions & 0 deletions src/winstanley/psi/impl/WdlPsiImplUtil.scala
@@ -0,0 +1,34 @@
package winstanley.psi.impl

import com.intellij.psi.{PsiElement, PsiReference}
import winstanley.psi.{WdlVariableLookup, WdlWorkflowBlock}
import winstanley.references.WdlDeclarationReference
import winstanley.structure.WdlImplicits._


/**
* This class is used by the .bnf compiler to implement the 'methods=[...]' methods on PsiElements.
*
* Put all your other junk util methods somewhere else!
*/
object WdlPsiImplUtil extends
WdlNamedElementImplUtil with
WdlVariableLookupImplUtil

/**
* Provides the getName, setName and getNameIdentifier methods for the WdlNamedElement subclasses (see (eg) declaration and scatter_declaration in the bnf)
*/
sealed trait WdlNamedElementImplUtil {
def getName(namedElement: WdlNamedElementImpl): String = namedElement.declaredValueName.orNull
// TODO: Implement for "refactor/rename" functionality
def setName(namedElement: WdlNamedElementImpl, newName: String): PsiElement = ???
def getNameIdentifier(namedElement: WdlNamedElementImpl): PsiElement = namedElement.getIdentifierNode.map(_.getPsi).orNull
}

sealed trait WdlVariableLookupImplUtil {
/**
* This is the method that enables the 'go to declaration' functionality for variable usages.
*/
def getReferences(wdlVariableLookup: WdlVariableLookup): Array[PsiReference] = Array(WdlDeclarationReference(wdlVariableLookup))
}

41 changes: 41 additions & 0 deletions src/winstanley/references/WdlDeclarationReference.scala
@@ -0,0 +1,41 @@
package winstanley.references

import javax.annotation.Nullable

import com.intellij.openapi.util.TextRange
import com.intellij.psi.{PsiElement, PsiReferenceBase}
import winstanley.psi.WdlVariableLookup
import winstanley.structure.WdlImplicits._

final case class WdlDeclarationReference(value: WdlVariableLookup) extends PsiReferenceBase[PsiElement](value, value.getTextRange){

/**
* Returns the element which is the target of the reference !!! OR NULL IF NOT FOUND !!!
*
* @return the target element, or null if it was not possible to resolve the reference to a valid target.
* @see PsiPolyVariantReference#multiResolve(boolean)
*/
@Nullable
override def resolve(): PsiElement = {
value.findDeclarationsAvailableInScope.find(d => value.getIdentifierNode.exists(_.getText == d.getNameIdentifier.getText)).map(_.getNameIdentifier).orNull
}

/**
* Returns the array of String, PsiElement and/or LookupElement
* instances representing all identifiers that are visible at the location of the reference. The contents
* of the returned array is used to build the lookup list for basic code completion. (The list
* of visible identifiers may not be filtered by the completion prefix string - the
* filtering is performed later by IDEA core.)
*
* @return the array of available identifiers.
*/
override def getVariants: Array[AnyRef] = Array.empty[AnyRef]

/**
* This override is required to make reference-lookup work.
*
* It needs a relative range within the PsiElement 'value' to count as the reference,
* which in this case is the entire 'WdlVariableLookup' element.
*/
override def getRangeInElement: TextRange = new TextRange(0, value.getTextLength - 1)
}
72 changes: 26 additions & 46 deletions src/winstanley/structure/WdlImplicits.scala
Expand Up @@ -8,32 +8,28 @@ import winstanley.psi._

object WdlImplicits {
implicit final class EnhancedPsiElement(val psiElement: PsiElement) extends AnyVal {
def childTaskBlocks: Set[WdlTaskBlock] = (psiElement.getChildren collect {
case t: WdlTaskBlock => t
}).toSet

def childWorkflowBlocks: Set[WdlWorkflowBlock] = (psiElement.getChildren collect {
case w: WdlWorkflowBlock => w
}).toSet
def contentRange: Option[TextRange] = {
def mapContainingContentRange(psiElement: PsiElement): Option[TextRange] = psiElement.getChildren.collectFirst {
case m: WdlMap => interBraceContentRange(m, WdlTypes.LBRACE, WdlTypes.RBRACE)
}.flatten

def contentRange: Option[TextRange] = psiElement match {
case _: WdlTaskBlock | _: WdlWorkflowBlock | _: WdlTaskOutputs | _: WdlWfOutputs | _: WdlCallBlock | _: WdlScatterBlock | _: WdlIfStmt =>
interBraceContentRange(psiElement, WdlTypes.LBRACE, WdlTypes.RBRACE)
case wcb: WdlCommandBlock => interBraceContentRange(wcb, WdlTypes.COMMAND_DELIMITER_OPEN, WdlTypes.COMMAND_DELIMITER_CLOSE)
case _: WdlRuntimeBlock | _: WdlParameterMetaBlock =>
mapContainingContentRange(psiElement)
case _ => None
}

private def mapContainingContentRange(psiElement: PsiElement): Option[TextRange] = psiElement.getChildren.collectFirst {
case m: WdlMap => interBraceContentRange(m, WdlTypes.LBRACE, WdlTypes.RBRACE)
}.flatten
def interBraceContentRange(psiElement: PsiElement, ltype: IElementType, rtype: IElementType): Option[TextRange] = {
for {
lbrace <- Option(psiElement.getNode.findChildByType(ltype))
rbrace <- Option(psiElement.getNode.findChildByType(rtype))
} yield new TextRange(lbrace.getTextRange.getStartOffset + 1, rbrace.getTextRange.getEndOffset - 1)
}

private def interBraceContentRange(psiElement: PsiElement, ltype: IElementType, rtype: IElementType): Option[TextRange] = {
for {
lbrace <- Option(psiElement.getNode.findChildByType(ltype))
rbrace <- Option(psiElement.getNode.findChildByType(rtype))
} yield new TextRange(lbrace.getTextRange.getStartOffset + 1, rbrace.getTextRange.getEndOffset - 1)
psiElement match {
case _: WdlTaskBlock | _: WdlWorkflowBlock | _: WdlTaskOutputs | _: WdlWfOutputs | _: WdlCallBlock | _: WdlScatterBlock | _: WdlIfStmt =>
interBraceContentRange(psiElement, WdlTypes.LBRACE, WdlTypes.RBRACE)
case wcb: WdlCommandBlock =>
interBraceContentRange(wcb, WdlTypes.COMMAND_DELIMITER_OPEN, WdlTypes.COMMAND_DELIMITER_CLOSE)
case _: WdlRuntimeBlock | _: WdlParameterMetaBlock =>
mapContainingContentRange(psiElement)
case _ => None
}
}

def findContainingScatter: Option[WdlScatterBlock] = {
Expand All @@ -54,7 +50,7 @@ object WdlImplicits {
psiElement.getChildren.toSet flatMap expandChild
}

def findDeclarationsAvailableInScope: Set[WdlDeclaration] = {
def findDeclarationsAvailableInScope: Set[WdlNamedElement] = {
Option(psiElement.getParent) map { parent =>
val siblings = parent.getChildren.filterNot(_ eq psiElement)
val siblingDeclarations = siblings collect {
Expand All @@ -64,31 +60,15 @@ object WdlImplicits {
case b: WdlWfBodyElement if b.getIfStmt != null => b.getIfStmt.findDeclarationsInInnerScopes
}

parent.findDeclarationsAvailableInScope ++ siblingDeclarations.flatten
} getOrElse Set.empty
}

def getIdentifierNode: Option[ASTNode] = psiElement.getNode.getChildren(null).collectFirst {
case id if id.getElementType == WdlTypes.IDENTIFIER => id
}
}

implicit final class EnhancedWdlDeclaration(val wdlDeclaration: WdlDeclaration) extends AnyVal {
// The Option-ality is a little paranoid, but if the declaration is being edited it might be temporarily nameless:
def declaredValueName: Option[String] = wdlDeclaration.getIdentifierNode.map(_.getText)
}
val scatterDeclaration = Option(parent) collect {
case b: WdlWfBodyElement if b.getScatterBlock != null => b.getScatterBlock.getScatterDeclaration
}

implicit final class EnhancedWdlValue(val wdlValue: WdlValue) extends AnyVal {
def asIdentifierNode: Option[ASTNode] = {
if (wdlValue.getChildren.isEmpty) wdlValue.getIdentifierNode else None
parent.findDeclarationsAvailableInScope ++ siblingDeclarations.flatten ++ scatterDeclaration
} getOrElse Set.empty
}
}

implicit final class EnhancedWdlTaskBlock(val wdlTaskBlock: WdlTaskBlock) extends AnyVal {
def taskName: String = wdlTaskBlock.getNode.findChildByType(winstanley.psi.WdlTypes.TASK_IDENTIFIER_DECL).getText
def getIdentifierNode: Option[ASTNode] = Option(psiElement.getNode.findChildByType(WdlTypes.IDENTIFIER))
}

implicit final class EnhancedWdlWorkflowBlock(val wdlWorkflowBlock: WdlWorkflowBlock) extends AnyVal {
def workflowName: String = wdlWorkflowBlock.getNode.findChildByType(winstanley.psi.WdlTypes.WORKFLOW_IDENTIFIER_DECL).getText
}
}
10 changes: 7 additions & 3 deletions src/winstanley/wdl.bnf
Expand Up @@ -11,6 +11,8 @@
elementTypeHolderClass="winstanley.psi.WdlTypes"
elementTypeClass="winstanley.psi.WdlElementType"
tokenTypeClass="winstanley.psi.WdlTokenType"

psiImplUtilClass="winstanley.psi.impl.WdlPsiImplUtil"
}

// Regenerate from IntelliJ using Grammar-Kit plugin and COMMAND-SHIFT-G
Expand All @@ -35,7 +37,8 @@ wf_output_wildcard ::= DOT ASTERISK

while_loop ::= WHILE LPAREN expression RPAREN LBRACE wf_body_element* RBRACE
if_stmt ::= IF LPAREN expression RPAREN LBRACE wf_body_element* RBRACE
scatter_block ::= SCATTER LPAREN IDENTIFIER IN expression RPAREN LBRACE wf_body_element* RBRACE
scatter_declaration ::= SCATTER LPAREN IDENTIFIER IN expression RPAREN {mixin="winstanley.psi.impl.WdlNamedElementImpl" implements="winstanley.psi.WdlNamedElement" methods=[getName getNameIdentifier setName]}
scatter_block ::= scatter_declaration LBRACE wf_body_element* RBRACE

task_block ::= TASK TASK_IDENTIFIER_DECL LBRACE declaration* sections* RBRACE
sections ::= command_block|task_outputs|runtime_block|parameter_meta_block|meta_block
Expand All @@ -55,7 +58,7 @@ runtime_block ::= RUNTIME map
parameter_meta_block ::= PARAMETER_META map
meta_block ::= META map

declaration ::= type_e IDENTIFIER setter?
declaration ::= type_e IDENTIFIER setter? {mixin="winstanley.psi.impl.WdlNamedElementImpl" implements="winstanley.psi.WdlNamedElement" methods=[getName getNameIdentifier setName]}
setter ::= EQUAL expression

map ::= LBRACE kv* RBRACE
Expand Down Expand Up @@ -84,7 +87,8 @@ array_literal ::= LSQUARE expression (COMMA expression)* RSQUARE

map_kv ::= expression COLON expression
object_kv ::= IDENTIFIER COLON expression
value ::= string_literal | IDENTIFIER | BOOLEAN | float_value | integer_value
value ::= string_literal | variable_lookup | BOOLEAN | float_value | integer_value
variable_lookup ::= IDENTIFIER {methods=[getReferences]}
float_value ::= NUMBER+ DOT NUMBER+
integer_value ::= NUMBER+

Expand Down

0 comments on commit c9e3913

Please sign in to comment.