Skip to content

Commit

Permalink
Insertion live template for connections (#120)
Browse files Browse the repository at this point in the history
Adds insertion live template support for GUI connect operations.
Experimental, partially implemented (for single ports only, limited
support for arrays and boundary ports), currently behind a toggle in the
settings menu.

Architecturally, this separates the connect-builder (which determines
which connections are allowed to a link) from the block analysis (which
determines which ports are in a connected group, and what connects are
available to a port), from the IR mutator (the executor, which takes a
connectivity change and updates the Block IR), from the code generator /
insertion live template. Probably more code to before, but in chunks
that actually make more sense. Prior, GUI connects were much more
monolithic code-wise.

Other functional changes:
- Fix getting the nearest insertion location when above the class level
- prior it crashed because it would traverse to the folder level and
can’t getTextRange. Now it detects a null text range is invalid.

Other refactoring / infrastructural changes:
- Updates to support the new connect expansion.
- Refactor the IR-to-graph parsing to use the expanded versions, if
available.
- Move the insert block live template to its own file
- Support no variables in insertion live template

Future TODOs
- Support suggested index - perhaps as template field
- Don’t allow name when connecting to an existing named link
- Support connect append in live template
- Support boundary ports including bridging
- Better support for array types
  • Loading branch information
ducky64 authored Aug 29, 2023
1 parent b80c424 commit c0cab42
Show file tree
Hide file tree
Showing 23 changed files with 1,928 additions and 187 deletions.
2 changes: 1 addition & 1 deletion PolymorphicBlocks
Submodule PolymorphicBlocks updated 27 files
+158 −45 compiler/src/main/scala/edg/EdgirUtils.scala
+43 −16 compiler/src/main/scala/edg/ElemBuilder.scala
+103 −119 compiler/src/main/scala/edg/compiler/Compiler.scala
+20 −10 compiler/src/main/scala/edg/compiler/DesignRefsValidate.scala
+37 −3 compiler/src/main/scala/edg/compiler/ExprToString.scala
+88 −10 compiler/src/main/scala/edg/compiler/ValueExprMap.scala
+1 −9 compiler/src/main/scala/edg/wir/Mixins.scala
+54 −9 compiler/src/test/scala/edg/compiler/CompilerBlockPortArrayExpansionTest.scala
+117 −41 compiler/src/test/scala/edg/compiler/CompilerLinkArrayExpansionTest.scala
+35 −11 compiler/src/test/scala/edg/compiler/CompilerLinkPortArrayExpansionTest.scala
+12 −2 compiler/src/test/scala/edg/compiler/TunnelExportTest.scala
+ edg_core/resources/edg-compiler-precompiled.jar
+28 −4 edg_core/test_design_inst.py
+1 −1 edgir/elem_pb2.pyi
+9 −9 edgir/expr_pb2.py
+16 −2 edgir/expr_pb2.pyi
+1 −0 electronics_abstract_parts/AbstractPowerConverters.py
+1 −1 electronics_abstract_parts/DigitalAmplifiers.py
+1 −1 electronics_abstract_parts/IoController.py
+1 −1 electronics_abstract_parts/SmdStandardPackage.py
+1 −0 electronics_lib/BoostConverter_TexasInstruments.py
+1 −1 electronics_lib/FanConnector.py
+1 −1 electronics_lib/Microcontroller_Lpc1549.py
+2 −2 electronics_lib/Microcontroller_Stm32f303.py
+1 −1 electronics_lib/Microcontroller_nRF52840.py
+14 −0 electronics_model/NetlistGenerator.py
+8 −0 proto/edgir/expr.proto
60 changes: 41 additions & 19 deletions src/main/scala/edg_ide/edgir_graph/EdgirGraph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,51 @@ object EdgirGraph {
): Seq[EdgirEdge] = {
constraints.flatMap { case (name, constr) =>
constr.expr match {
case expr.ValueExpr.Expr.Connected(connect) =>
// in the loading pass, the source is the block side and the target is the link side
Some(
EdgirEdge(
ConnectWrapper(path + name, constr),
source = Ref.unapply(connect.getBlockPort.getRef).get.slice(0, 2), // only block and port, ignore arrays
target = Ref.unapply(connect.getLinkPort.getRef).get.slice(0, 2)
)
)
case expr.ValueExpr.Expr.Exported(export) =>
// in the loading pass, the source is the block side and the target is the external port
Some(
EdgirEdge(
ConnectWrapper(path + name, constr),
source = Ref.unapply(export.getInternalBlockPort.getRef).get.slice(0, 2),
target = Ref.unapply(`export`.getExteriorPort.getRef).get.slice(0, 1)
)
)
case _ => None
case expr.ValueExpr.Expr.Connected(connected) =>
connectedToEdge(path, name, constr, connected)
case expr.ValueExpr.Expr.Exported(exported) =>
exportedToEdge(path, name, constr, exported)
case expr.ValueExpr.Expr.ConnectedArray(connectedArray) =>
connectedArray.expanded.flatMap(connectedToEdge(path, name, constr, _))
case expr.ValueExpr.Expr.ExportedArray(exportedArray) =>
exportedArray.expanded.flatMap(exportedToEdge(path, name, constr, _))
case _ => Seq()
}
}.toSeq
}

protected def connectedToEdge(
path: DesignPath,
constrName: String,
constr: expr.ValueExpr,
connected: expr.ConnectedExpr
): Seq[EdgirEdge] = connected.expanded match {
case Seq() => Seq( // in the loading pass, the source is the block side and the target is the link side
EdgirEdge(
ConnectWrapper(path + constrName, constr),
source = Ref.unapply(connected.getBlockPort.getRef).get.slice(0, 2), // only block and port, ignore arrays
target = Ref.unapply(connected.getLinkPort.getRef).get.slice(0, 2)
))
case Seq(expanded) => connectedToEdge(path, constrName, constr, expanded)
case _ => throw new IllegalArgumentException("unexpected multiple expanded")
}

protected def exportedToEdge(
path: DesignPath,
constrName: String,
constr: expr.ValueExpr,
exported: expr.ExportedExpr
): Seq[EdgirEdge] = exported.expanded match {
case Seq() => Seq( // in the loading pass, the source is the block side and the target is the external port
EdgirEdge(
ConnectWrapper(path + constrName, constr),
source = Ref.unapply(exported.getInternalBlockPort.getRef).get.slice(0, 2),
target = Ref.unapply(exported.getExteriorPort.getRef).get.slice(0, 1)
))
case Seq(expanded) => exportedToEdge(path, constrName, constr, expanded)
case _ => throw new IllegalArgumentException("unexpected multiple expanded")
}

def blockLikeToNode(path: DesignPath, blockLike: elem.BlockLike): EdgirNode = {
blockLike.`type` match {
case elem.BlockLike.Type.Hierarchy(block) =>
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/edg_ide/psi_edits/InsertAction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object InsertAction {

// given the leaf element at the caret, returns the rootmost element right before the caret
def prevElementOf(element: PsiElement): PsiElement = {
requireExcept(element.getTextRange != null, "element with null range") // if traversing beyond file level
if (element.getTextRange.getStartOffset == caretOffset) { // caret at beginning of element, so take the previous
val prev = PsiTreeUtil.prevLeaf(element)
prevElementOf(prev.exceptNull("no element before caret"))
Expand Down
152 changes: 3 additions & 149 deletions src/main/scala/edg_ide/psi_edits/InsertBlockAction.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package edg_ide.psi_edits

import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.openapi.application.{ModalityState, ReadAction}
import com.intellij.openapi.command.WriteCommandAction.writeCommandAction
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.{PsiElement, PsiWhiteSpace}
import com.intellij.util.concurrency.AppExecutorUtil
import com.jetbrains.python.psi._
import edg.util.Errorable
import edg_ide.util.ExceptionNotifyImplicits.{ExceptNotify, ExceptOption, ExceptSeq}
import edg_ide.util.{DesignAnalysisUtils, exceptable, requireExcept}
import edg_ide.util.ExceptionNotifyImplicits.{ExceptNotify, ExceptSeq}
import edg_ide.util.{DesignAnalysisUtils, exceptable}

import java.util.concurrent.Callable

Expand Down Expand Up @@ -111,149 +110,4 @@ object InsertBlockAction {
() => insertBlockFlow
}

/** Creates an action to start a live template to insert a block.
*/
def createTemplateBlock(
contextClass: PyClass,
libClass: PyClass,
actionName: String,
project: Project,
continuation: (String, PsiElement) => Unit
): Errorable[() => Unit] = exceptable {
val languageLevel = LanguageLevel.forElement(libClass)
val psiElementGenerator = PyElementGenerator.getInstance(project)

// given some caret position, returns the best insertion position
def getInsertionElt(caretEltOpt: Option[PsiElement]): PsiElement = {
exceptable { // TODO better propagation of error messages
val caretElt = caretEltOpt.exceptNone("no elt at caret")
val caretStatement = InsertAction.snapInsertionEltOfType[PyStatement](caretElt).get
val containingPsiFn = PsiTreeUtil
.getParentOfType(caretStatement, classOf[PyFunction])
.exceptNull(s"caret not in a function")
val containingPsiClass = PsiTreeUtil
.getParentOfType(containingPsiFn, classOf[PyClass])
.exceptNull(s"caret not in a class")
requireExcept(containingPsiClass == contextClass, s"caret not in class of type ${libClass.getName}")
caretStatement
}.toOption.orElse {
val candidates =
InsertAction.findInsertionElements(contextClass, InsertBlockAction.VALID_FUNCTION_NAMES)
candidates.headOption
}.get // TODO insert contents() if needed
}

val movableLiveTemplate = new MovableLiveTemplate(actionName) {
override def startTemplate(caretEltOpt: Option[PsiElement]): InsertionLiveTemplate = {
val insertAfter = getInsertionElt(caretEltOpt)
val containingPsiFn = PsiTreeUtil.getParentOfType(insertAfter, classOf[PyFunction])
val containingPsiClass = PsiTreeUtil.getParentOfType(containingPsiFn, classOf[PyClass])
val selfName = containingPsiFn.getParameterList.getParameters.toSeq
.exceptEmpty(s"function ${containingPsiFn.getName} has no self")
.head
.getName

val newAssignTemplate = psiElementGenerator.createFromText(
languageLevel,
classOf[PyAssignmentStatement],
s"$selfName.name = $selfName.Block(${libClass.getName}())"
)
val containingStmtList = PsiTreeUtil.getParentOfType(insertAfter, classOf[PyStatementList])

val newAssign =
containingStmtList.addAfter(newAssignTemplate, insertAfter).asInstanceOf[PyAssignmentStatement]
val newArgList = newAssign.getAssignedValue
.asInstanceOf[PyCallExpression]
.getArgument(0, classOf[PyCallExpression])
.getArgumentList

val initParams =
DesignAnalysisUtils.initParamsOf(libClass, project).toOption.getOrElse((Seq(), Seq()))
val allParams = initParams._1 ++ initParams._2

val nameTemplateVar = new InsertionLiveTemplate.Reference(
"name",
newAssign.getTargets.head.asInstanceOf[PyTargetExpression],
InsertionLiveTemplate.validatePythonName(_, _, Some(containingPsiClass)),
defaultValue = Some("")
)

val argTemplateVars = allParams.map { initParam =>
val paramName = initParam.getName() + (Option(initParam.getAnnotationValue) match {
case Some(typed) => f": $typed"
case None => ""
})

if (initParam.getDefaultValue == null) { // required argument, needs ellipsis
newArgList.addArgument(psiElementGenerator.createEllipsis())
new InsertionLiveTemplate.Variable(paramName, newArgList.getArguments.last)
} else { // optional argument
// ellipsis is generated in the AST to give the thing a handle, the template replaces it with an empty
newArgList.addArgument(
psiElementGenerator.createKeywordArgument(languageLevel, initParam.getName, "...")
)
new InsertionLiveTemplate.Variable(
f"$paramName (optional)",
newArgList.getArguments.last.asInstanceOf[PyKeywordArgument].getValueExpression,
defaultValue = Some("")
)
}
}

new InsertionLiveTemplate(newAssign, IndexedSeq(nameTemplateVar) ++ argTemplateVars)
}
}

movableLiveTemplate.addTemplateStateListener(new TemplateFinishedListener {
override def templateFinished(state: TemplateState, brokenOff: Boolean): Unit = {
val expr = state.getExpressionContextForSegment(0)
if (expr.getTemplateEndOffset <= expr.getTemplateStartOffset) {
return // ignored if template was deleted, including through moving the template
}

val insertedName = state.getVariableValue("name").getText
if (insertedName.isEmpty && brokenOff) { // canceled by esc while name is empty
writeCommandAction(project)
.withName(s"cancel $actionName")
.compute(() => {
TemplateUtils.deleteTemplate(state)
})
} else {
var templateElem = state.getExpressionContextForSegment(0).getPsiElementAtStartOffset
while (templateElem.isInstanceOf[PsiWhiteSpace]) { // ignore inserted whitespace before the statement
templateElem = templateElem.getNextSibling
}
try {
val args = templateElem
.asInstanceOf[PyAssignmentStatement]
.getAssignedValue
.asInstanceOf[PyCallExpression] // the self.Block(...) call
.getArgument(0, classOf[PyCallExpression]) // the object instantiation
.getArgumentList // args to the object instantiation
val deleteArgs = args.getArguments.flatMap { // remove empty kwargs
case arg: PyKeywordArgument => if (arg.getValueExpression == null) Some(arg) else None
case _ => None // ignored
}
writeCommandAction(project)
.withName(s"clean $actionName")
.compute(() => {
deleteArgs.foreach(_.delete())
})
} finally {
continuation(insertedName, templateElem)
}
}
}
})

val caretElt = InsertAction.getCaretForNewClassStatement(contextClass, project).toOption
def insertBlockFlow: Unit = {
writeCommandAction(project)
.withName(s"$actionName")
.compute(() => {
movableLiveTemplate.run(caretElt)
})
}
() => insertBlockFlow
}
}
5 changes: 3 additions & 2 deletions src/main/scala/edg_ide/psi_edits/InsertionLiveTemplate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,10 @@ class InsertionLiveTemplate(elt: PsiElement, variables: IndexedSeq[InsertionLive
// if the editor just started, it isn't marked as showing and the tooltip creation crashes
// TODO the positioning is still off, but at least it doesn't crash
UIUtil.markAsShowing(editor.getContentComponent, true)
val firstVariableName = variables.headOption.map(_.name).getOrElse("")
val tooltipString = initialTooltip match {
case Some(initialTooltip) => f"${variables.head.name} | $initialTooltip"
case None => f"${variables.head.name}"
case Some(initialTooltip) => f"$firstVariableName | $initialTooltip"
case None => firstVariableName
}
val tooltip = createTemplateTooltip(tooltipString, editor)

Expand Down
Loading

0 comments on commit c0cab42

Please sign in to comment.