Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/canton-jetbrains-plugin/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pluginGroup = com.moonsonglabs.daml
pluginName = canton-jetbrains-plugin
pluginVersion = 0.1.0
pluginVersion = 0.2.0

# IntelliJ IDEA Community as the target SDK so the plugin runs in any JetBrains IDE >= 2025.2.
platformType = IC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import com.intellij.psi.tree.IElementType
*
* Recognizes: line comments (-- ...), block comments ({- ... -} with nesting), pragmas
* ({-# ... #-}), doc comments (-- | ... and {-| ... -}), string literals, char literals,
* numeric literals, keywords (Haskell + DAML), uppercase identifiers as types, operators,
* brackets, and whitespace.
* numeric literals, keyword groups, booleans, Prelude types/constructors, uppercase
* identifiers as types, common DAML operators, brackets, and whitespace.
*/
class DamlLexer : LexerBase() {

Expand Down Expand Up @@ -54,6 +54,8 @@ class DamlLexer : LexerBase() {
c == '"' -> consumeStringLiteral()
c == '\'' -> consumeCharLiteral()
c.isDigit() -> consumeNumber()
c == '(' && peek(1) == ')' -> { pos += 2; tokenType = DamlTokenTypes.UNIT_LITERAL }
c == '[' && peek(1) == ']' -> { pos += 2; tokenType = DamlTokenTypes.EMPTY_LIST_LITERAL }
c == '(' -> { pos++; tokenType = DamlTokenTypes.LPAREN }
c == ')' -> { pos++; tokenType = DamlTokenTypes.RPAREN }
c == '{' -> { pos++; tokenType = DamlTokenTypes.LBRACE }
Expand Down Expand Up @@ -169,17 +171,36 @@ class DamlLexer : LexerBase() {
while (pos < endOffset && isIdCont(buffer[pos])) pos++
val text = buffer.subSequence(tokenStart, pos).toString()
tokenType = when {
text in DamlKeywords.booleanLiterals -> DamlTokenTypes.BOOLEAN_LITERAL
text in DamlKeywords.controlKeywords -> DamlTokenTypes.CONTROL_KEYWORD
text in DamlKeywords.damlKeywords -> DamlTokenTypes.DAML_KEYWORD
text in DamlKeywords.haskellKeywords -> DamlTokenTypes.KEYWORD
text in DamlKeywords.choiceModifierKeywords -> DamlTokenTypes.CHOICE_MODIFIER_KEYWORD
text in DamlKeywords.moduleKeywords -> DamlTokenTypes.MODULE_KEYWORD
text in DamlKeywords.importKeywords -> DamlTokenTypes.IMPORT_KEYWORD
text in DamlKeywords.contractClauseKeywords -> DamlTokenTypes.DAML_KEYWORD
text in DamlKeywords.declarationKeywords -> DamlTokenTypes.DECLARATION_KEYWORD
text in DamlKeywords.haskellKeywords || text in DamlKeywords.damlKeywords -> DamlTokenTypes.KEYWORD
text in DamlKeywords.predefinedConstructors -> DamlTokenTypes.PREDEFINED_IDENTIFIER
text in DamlKeywords.preludeTypes -> DamlTokenTypes.PRELUDE_TYPE
text in DamlKeywords.builtins -> DamlTokenTypes.BUILTIN_IDENTIFIER
firstChar.isUpperCase() -> DamlTokenTypes.TYPE_NAME
else -> DamlTokenTypes.IDENTIFIER
}
}

private fun consumeOperator() {
while (pos < endOffset && isOpChar(buffer[pos])) pos++
tokenType = DamlTokenTypes.OPERATOR
val text = buffer.subSequence(tokenStart, pos).toString()
tokenType = when (text) {
"." -> DamlTokenTypes.DOT
":" -> DamlTokenTypes.COLON
"::" -> DamlTokenTypes.DOUBLE_COLON
"->", "\u2192" -> DamlTokenTypes.ARROW
"=>", "\u21d2" -> DamlTokenTypes.BIG_ARROW
"<-" -> DamlTokenTypes.BIND_ARROW
"=" -> DamlTokenTypes.EQUALS
"==", "/=" -> DamlTokenTypes.EQUALITY_OPERATOR
else -> DamlTokenTypes.OPERATOR
}
}

private fun isIdStart(c: Char): Boolean = c.isLetter() || c == '_'
Expand All @@ -192,7 +213,8 @@ class DamlLexer : LexerBase() {
}

private fun isOpChar(c: Char): Boolean = when (c) {
'!', '#', '$', '%', '&', '*', '+', '.', '/', '<', '=', '>', '?', '@', '\\', '^', '|', '-', '~', ':' -> true
'!', '#', '$', '%', '&', '*', '+', '.', '/', '<', '=', '>', '?', '@', '\\', '^', '|', '-', '~', ':',
'\u2192', '\u21d2' -> true
else -> false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,35 @@ object DamlTokenTypes {
@JvmField val PRAGMA = DamlTokenType("PRAGMA")

@JvmField val KEYWORD = DamlTokenType("KEYWORD")
@JvmField val MODULE_KEYWORD = DamlTokenType("MODULE_KEYWORD")
@JvmField val IMPORT_KEYWORD = DamlTokenType("IMPORT_KEYWORD")
@JvmField val DECLARATION_KEYWORD = DamlTokenType("DECLARATION_KEYWORD")
@JvmField val DAML_KEYWORD = DamlTokenType("DAML_KEYWORD")
@JvmField val CHOICE_MODIFIER_KEYWORD = DamlTokenType("CHOICE_MODIFIER_KEYWORD")
@JvmField val CONTROL_KEYWORD = DamlTokenType("CONTROL_KEYWORD")

@JvmField val TYPE_NAME = DamlTokenType("TYPE_NAME")
@JvmField val PRELUDE_TYPE = DamlTokenType("PRELUDE_TYPE")
@JvmField val IDENTIFIER = DamlTokenType("IDENTIFIER")
@JvmField val BUILTIN_IDENTIFIER = DamlTokenType("BUILTIN_IDENTIFIER")
@JvmField val PREDEFINED_IDENTIFIER = DamlTokenType("PREDEFINED_IDENTIFIER")

@JvmField val STRING_LITERAL = DamlTokenType("STRING_LITERAL")
@JvmField val CHAR_LITERAL = DamlTokenType("CHAR_LITERAL")
@JvmField val NUMBER = DamlTokenType("NUMBER")
@JvmField val BOOLEAN_LITERAL = DamlTokenType("BOOLEAN_LITERAL")
@JvmField val UNIT_LITERAL = DamlTokenType("UNIT_LITERAL")
@JvmField val EMPTY_LIST_LITERAL = DamlTokenType("EMPTY_LIST_LITERAL")

@JvmField val OPERATOR = DamlTokenType("OPERATOR")
@JvmField val DOT = DamlTokenType("DOT")
@JvmField val COLON = DamlTokenType("COLON")
@JvmField val DOUBLE_COLON = DamlTokenType("DOUBLE_COLON")
@JvmField val ARROW = DamlTokenType("ARROW")
@JvmField val BIG_ARROW = DamlTokenType("BIG_ARROW")
@JvmField val BIND_ARROW = DamlTokenType("BIND_ARROW")
@JvmField val EQUALS = DamlTokenType("EQUALS")
@JvmField val EQUALITY_OPERATOR = DamlTokenType("EQUALITY_OPERATOR")
@JvmField val LPAREN = DamlTokenType("LPAREN")
@JvmField val RPAREN = DamlTokenType("RPAREN")
@JvmField val LBRACE = DamlTokenType("LBRACE")
Expand Down Expand Up @@ -64,6 +82,16 @@ object DamlTokenTypes {
* pulling the full TextMate grammar.
*/
object DamlKeywords {
val moduleKeywords = setOf("module", "where")

val importKeywords = setOf("import", "qualified", "as", "hiding")

val declarationKeywords = setOf(
"data", "newtype", "type", "class", "instance", "deriving",
"default", "infix", "infixl", "infixr", "forall",
"template", "interface", "exception"
)

val haskellKeywords = setOf(
"module", "where", "import", "qualified", "as", "hiding",
"data", "newtype", "type", "class", "instance", "deriving",
Expand All @@ -76,13 +104,80 @@ object DamlKeywords {
val damlKeywords = setOf(
"template", "with", "choice", "controller", "can",
"signatory", "observer", "agreement", "ensure",
"key", "maintainer",
"key", "maintainer", "message", "magreement",
"nonconsuming", "preconsuming", "postconsuming",
"interface", "viewtype", "requires", "implements", "coimplements",
"exception", "for"
)

val contractClauseKeywords = setOf(
"with", "choice", "controller", "can",
"signatory", "observer", "agreement", "ensure",
"key", "maintainer", "message", "magreement",
"viewtype", "requires", "implements", "coimplements", "for"
)

val choiceModifierKeywords = setOf("nonconsuming", "preconsuming", "postconsuming")

val controlKeywords = setOf("do", "if", "then", "else", "case", "of", "try", "catch")

val all = haskellKeywords + damlKeywords
val booleanLiterals = setOf("True", "False")

val preludeTypes = setOf(
"Any", "AnyChoice", "AnyContractKey", "AnyTemplate",
"AnyContractId", "Archive", "Bool", "Choice", "Commands", "ContractId",
"CryptoErrorType", "Date", "DayOfWeek", "Decimal", "Disclosure", "Either",
"Exercised", "GenMap", "Int", "List", "Map", "Month", "Numeric", "Optional",
"PackageId", "ParticipantName", "Party", "PartyDetails", "PartyIdHint",
"PrivateKeyHex", "RelTime", "Script", "Secp256k1KeyPair", "SubmitError",
"SubmitOptions", "Template", "TemplateKey", "TemplateTypeRep", "Text", "TextMap",
"Time", "TransactionTree", "TreeEvent", "TreeIndex", "Update", "UpgradeErrorType",
"User", "UserAlreadyExists", "UserId", "UserNotFound", "UserRight"
)

val predefinedConstructors = setOf(
"Some", "None", "Left", "Right", "LT", "EQ", "GT",
"CanActAs", "CanReadAs", "CanReadAsAnyParty", "ParticipantAdmin",
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
)

val builtins = setOf(
"script",
"create", "createAndExercise", "exercise", "exerciseByKey", "fetch", "fetchByKey",
"lookupByKey", "visibleByKey", "archive", "abort", "assert", "assertMsg",
"getTime", "return", "pure", "debug", "debugRaw",
"date", "datetime", "time", "subTime", "wholeDays",
"optional", "fromOptional", "fromSome", "fromSomeNote", "isNone", "isSome",
"createCmd", "createExactCmd", "exerciseCmd", "exerciseExactCmd",
"exerciseByKeyCmd", "exerciseByKeyExactCmd", "createAndExerciseCmd",
"createAndExerciseExactCmd", "createAndExerciseWithCidCmd",
"createAndExerciseWithCidExactCmd", "archiveCmd",
"submit", "submitWithOptions", "submitMustFail", "submitMustFailWithOptions",
"submitMulti", "submitMultiMustFail", "submitTree", "submitTreeMulti",
"submitResultAndTree", "submitWithDisclosures", "submitWithDisclosuresMustFail",
"submitWithError", "trySubmit", "trySubmitMulti", "trySubmitResultAndTree",
"trySubmitTree",
"query", "queryContractId", "queryContractKey", "queryDisclosure", "queryFilter",
"queryInterface", "queryInterfaceContractId",
"allocateParty", "allocatePartyByHint", "allocatePartyOn", "allocatePartyWithHint",
"allocatePartyByHintOn", "allocatePartyWithHintOn", "actAs", "readAs",
"disclose", "discloseMany", "prefetchKeys",
"concurrently", "partyFromText", "validateUserId", "createUser",
"createUserOn", "deleteUser", "deleteUserOn", "getUser", "getUserOn",
"grantUserRights", "grantUserRightsOn", "listAllUsers", "listAllUsersOn",
"listKnownParties", "listKnownPartiesOn", "listUserRights", "listUserRightsOn",
"revokeUserRights", "revokeUserRightsOn", "submitUser", "submitUserOn",
"tryFailureStatus", "tryToEither", "userIdToText",
"passTime", "setTime", "sleep",
"created", "createdN", "exercised", "exercisedN", "fromAnyContractId", "fromTree",
"packagePreference",
"toAnyChoice", "fromAnyChoice", "toAnyContractKey", "fromAnyContractKey",
"toAnyTemplate", "fromAnyTemplate", "toInterface", "fromInterface",
"toInterfaceContractId", "fromInterfaceContractId", "coerceInterfaceContractId",
"fetchFromInterface", "interfaceTypeRep", "view"
)

val all = haskellKeywords + damlKeywords + booleanLiterals
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class DamlLanguageClient(project: Project) : LanguageClientImpl(project) {
@JsonNotification("daml/virtualResource/note")
fun virtualResourceNote(payload: Map<String, Any?>) {
val uri = payload["uri"] as? String ?: return
val note = payload["note"] as? String ?: return
val note = (payload["note"] ?: payload["contents"] ?: payload["message"]) as? String ?: return
VirtualResourceManager.getInstance(project).note(uri, note)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ class DamlModuleReferenceContributor : PsiReferenceContributor() {
private class DamlModuleReferenceProvider : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
val type = element.node?.elementType
if (type != DamlTokenTypes.TYPE_NAME && type != DamlTokenTypes.IDENTIFIER && type != DamlTokenTypes.OPERATOR) {
if (type != DamlTokenTypes.TYPE_NAME &&
type != DamlTokenTypes.PRELUDE_TYPE &&
type != DamlTokenTypes.IDENTIFIER &&
type != DamlTokenTypes.OPERATOR &&
type != DamlTokenTypes.DOT
) {
return PsiReference.EMPTY_ARRAY
}
if (type == DamlTokenTypes.OPERATOR && element.text != ".") return PsiReference.EMPTY_ARRAY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import java.util.Base64
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.SwingConstants
import javax.swing.UIManager

/**
* The Script Results panel.
*
* Renders server-pushed HTML in a JCEF browser, exposing the same host/webview bridge that
* VSCode's webview uses (`set_show_archived`, `set_show_detailed_disclosure`,
* `set_selected_view`, plus hostguest `set_view`/`add_note`) and adapting VSCode's
* `set_selected_view`, plus host-to-guest `set_view`/`add_note`) and adapting VSCode's
* `command:daml.revealLocation` source links for JetBrains.
*
* If JCEF is unavailable on this IDE (e.g. on Linux without the JCEF runtime), falls back
Expand All @@ -43,6 +44,10 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
private val browser: JBCefBrowser?
private val jsQuery: JBCefJSQuery?
private val gson = Gson()
private var webviewReady = false
private var latestHtml: String? = null
private var latestProgress: Long? = null
private val pendingNotes = mutableListOf<String>()
private var titleLabel = JLabel("DAML Script Results", SwingConstants.LEFT).apply {
border = javax.swing.BorderFactory.createEmptyBorder(4, 8, 4, 8)
}
Expand All @@ -51,7 +56,7 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
if (!JBCefApp.isSupported()) {
browser = null
jsQuery = null
background = Color.WHITE
background = UIManager.getColor("Panel.background") ?: Color.WHITE
add(JLabel(DamlBundle.message("daml.notification.jcef.unavailable"),
SwingConstants.CENTER), BorderLayout.CENTER)
} else {
Expand All @@ -67,7 +72,9 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
b.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
override fun onLoadEnd(cefBrowser: CefBrowser?, frame: org.cef.browser.CefFrame?, httpStatusCode: Int) {
installBridge(cefBrowser)
webviewReady = true
sendInitialView()
flushPendingMessages()
}
}, b.cefBrowser)
add(titleLabel, BorderLayout.NORTH)
Expand All @@ -86,14 +93,23 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),

fun setHtml(html: String) {
if (browser == null) return
val js = "if (window.setHtmlContent) setHtmlContent(${gson.toJson(html)});"
browser.cefBrowser.executeJavaScript(js, browser.cefBrowser.url, 0)
latestHtml = html
if (webviewReady) dispatchHtml(html)
}

fun setNote(html: String) {
if (browser == null) return
val msg = mapOf("command" to "add_note", "value" to html)
postToWebview(msg)
if (webviewReady) {
dispatchNote(html)
} else {
pendingNotes.add(html)
}
}

fun setProgress(millisecondsPassed: Long) {
if (browser == null) return
latestProgress = millisecondsPassed
if (webviewReady) dispatchProgress(millisecondsPassed)
}

private fun loadInitialHtml() {
Expand All @@ -108,6 +124,7 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
val html = String(htmlBytes, StandardCharsets.UTF_8)
.replace("\$webviewSrc", jsDataUrl)
.replace("\$webviewCss", cssDataUrl)
.replace("\$webviewTheme", webviewThemeClass())
browser?.loadHTML(html)
}

Expand All @@ -128,12 +145,42 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
"value" to mapOf(
"selected" to s.selectedView,
"showArchived" to s.showArchived,
"showDetailedDisclosure" to s.showDetailedDisclosure
"showDetailedDisclosure" to s.showDetailedDisclosure,
"theme" to webviewThemeClass()
)
)
postToWebview(msg)
}

private fun flushPendingMessages() {
latestHtml?.let(::dispatchHtml)
pendingNotes.forEach(::dispatchNote)
pendingNotes.clear()
latestProgress?.let(::dispatchProgress)
}

private fun dispatchHtml(html: String) {
val b = browser ?: return
val js = "if (window.setHtmlContent) setHtmlContent(${gson.toJson(html)});"
b.cefBrowser.executeJavaScript(js, b.cefBrowser.url, 0)
}

private fun dispatchNote(html: String) {
val msg = mapOf("command" to "add_note", "value" to html)
postToWebview(msg)
}

private fun dispatchProgress(millisecondsPassed: Long) {
val msg = mapOf("command" to "set_progress", "value" to millisecondsPassed)
postToWebview(msg)
}

private fun webviewThemeClass(): String {
val color = UIManager.getColor("Panel.background") ?: background ?: Color.WHITE
val luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue
return if (luminance < 128) "ide-dark" else "ide-light"
}

private fun postToWebview(msg: Map<String, Any?>) {
val b = browser ?: return
val payload = gson.toJson(msg)
Expand All @@ -149,7 +196,7 @@ class ScriptResultsPanel(private val project: Project) : JPanel(BorderLayout()),
when (map["command"] as? String) {
"set_show_archived" -> s.showArchived = (map["value"] as? Boolean) ?: false
"set_show_detailed_disclosure" -> s.showDetailedDisclosure = (map["value"] as? Boolean) ?: false
"set_selected_view" -> s.selectedView = (map["value"] as? String) ?: "table"
"set_selected_view" -> s.selectedView = (map["value"] as? String) ?: "overview"
"reveal_location" -> revealLocation(map["value"] as? String)
else -> thisLogger().debug("Unhandled host message: $raw")
}
Expand Down
Loading
Loading