diff --git a/README.md b/README.md index d46711a..edebdf4 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,97 @@ To use the Context Mapper IntelliJ plugin you need the following tools (besides * [Oracle Java](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) or [OpenJDK](https://openjdk.java.net/) (JRE 11 or newer) * [Node.js](https://nodejs.org/en/download) (v22) * Maybe you want to install a [PlantUML extension](https://plugins.jetbrains.com/plugin/7017-plantuml-integration) for the generated PlantUML diagrams. +* [Graphviz](https://graphviz.org/) if you want to render generated PlantUML diagrams in IntelliJ. * LSP4IJ IntelliJ plugin (will be installed automatically when installing our plugin) +## Getting Started +This plugin provides Context Mapper DSL support in IntelliJ. It recognizes `.cml` files, starts the bundled Context Mapper language server through LSP4IJ, provides semantic highlighting, and can ask the language server to generate PlantUML `.puml` diagrams. + +### 1. Install the IntelliJ plugins +Install the Context Mapper plugin ZIP in IntelliJ: + +1. Open **Settings > Plugins**. +2. Click the gear icon and choose **Install Plugin from Disk...**. +3. Select the built plugin ZIP from `build/distributions/context-mapper-intellij-plugin.zip`. +4. Restart IntelliJ when prompted. + +The Context Mapper plugin depends on LSP4IJ. IntelliJ should install LSP4IJ automatically with this plugin. If syntax highlighting or generation does not work, check that both plugins are enabled under **Settings > Plugins**. + +### 2. Install local tools +The Context Mapper language server is a Node.js process. Make sure `node` is installed and visible to IntelliJ: + +```bash +node --version +which node +``` + +On macOS with Homebrew, Node is commonly installed under `/opt/homebrew/bin/node`. If IntelliJ was launched from Finder and cannot find Node, restart IntelliJ after installing Node or set the `CONTEXT_MAPPER_NODE` environment variable to the full Node executable path before launching IntelliJ. + +To preview generated PlantUML diagrams inside IntelliJ, install the PlantUML Integration plugin and Graphviz: + +```bash +brew install graphviz +which dot +dot -V +``` + +Then configure the PlantUML plugin's Graphviz/Dot executable path to the result of `which dot`, for example `/opt/homebrew/bin/dot`. A common broken default is `/opt/local/bin/dot`, which is a MacPorts path and may not exist on Homebrew-based systems. + +### 3. Create a simple CML file +Create a file such as `architecture/context-maps/example.cml`: + +```cml +ContextMap ExampleMap { + contains OrderingContext + contains BillingContext + + OrderingContext [SK]<->[SK] BillingContext +} + +BoundedContext OrderingContext { + type = APPLICATION + domainVisionStatement = "Handles customer order placement." +} + +BoundedContext BillingContext { + type = APPLICATION + domainVisionStatement = "Handles invoicing and payment workflows." +} +``` + +Syntax highlighting should appear after the file opens. If the file icon appears but keywords are not colored, the file type is registered but the language server may not be running. + +### 4. Generate a PlantUML diagram +Open the `.cml` file in the editor, then run: + +1. Right-click inside the editor. +2. Choose **Context Mapper > Generate PlantUML Diagrams**. + +You can also search for the action with **Cmd+Shift+A** and run **Generate PlantUML Diagrams**. + +The generated file is written to: + +```text +/src-gen/.puml +``` + +For example: + +```text +src-gen/example.puml +``` + +Open that `.puml` file to preview it with the PlantUML plugin. + +### Troubleshooting +If `.cml` files have the Context Mapper icon but no syntax highlighting, verify that LSP4IJ is enabled and that IntelliJ can start Node. Search the IntelliJ log for `cml-language-server`, `contextmapper`, `LSP4IJ`, or `node`. + +If generation reports that no PlantUML diagrams were created, make sure the `.cml` file contains a `ContextMap`. Standalone `BoundedContext` declarations are valid CML, but the current generator creates a component diagram from the first `ContextMap` in the file. + +If the PlantUML preview shows `Cannot find Graphviz`, the generated `.puml` file is usually fine. Configure the PlantUML plugin's Dot executable to the path returned by `which dot`. + ## Build and/or Run Extension Locally This project uses [Gradle](https://gradle.org/) to build the IntelliJ plugin. @@ -41,6 +128,18 @@ After cloning this repository, you can build the project with the following comm ./gradlew clean buildPlugin ``` +Before installing a built plugin ZIP into a newer IntelliJ IDEA version, verify plugin compatibility: + +```bash +./gradlew verifyPlugin +``` + +The verifier target defaults to IntelliJ IDEA Ultimate 2026.1.3. Override it when needed: + +```bash +./gradlew verifyPlugin -PintellijVerifierIdeVersion= +``` + Use the following command to build and run the plugin: ```bash diff --git a/build.gradle.kts b/build.gradle.kts index 860a267..8450243 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,6 +84,12 @@ intellijPlatform { publishing { token = System.getenv("PUBLISH_TOKEN") } + + pluginVerification { + ides { + ide(providers.provider { "IU" }, providers.gradleProperty("intellijVerifierIdeVersion").orElse("2026.1.3")) + } + } } kotlin { diff --git a/gradle.properties b/gradle.properties index 0f05f7b..44b578a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,9 +7,9 @@ org.gradle.caching=true intelliJVersion=2024.2.6 ideaSinceBuild=241 -ideaUntilBuild=251.* +ideaUntilBuild=261.* cmlVersion=6.12.0 lsp4ijVersion=0.13.0 mockkVersion=1.13.17 -archUnitVersion=1.4.1 \ No newline at end of file +archUnitVersion=1.4.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 81aa1c0..327d640 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Jun 16 16:37:07 AEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lsp/.npmrc b/lsp/.npmrc new file mode 100644 index 0000000..a1593d3 --- /dev/null +++ b/lsp/.npmrc @@ -0,0 +1 @@ +@contextmapper:registry=https://npm.pkg.github.com diff --git a/src/main/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServer.kt b/src/main/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServer.kt index 0fc1780..94a0148 100644 --- a/src/main/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServer.kt +++ b/src/main/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServer.kt @@ -3,13 +3,36 @@ package org.contextmapper.intellij.lsp4ij.server import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.extensions.PluginDescriptor import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider +import java.nio.file.Files +import kotlin.io.path.Path + +internal const val CONTEXT_MAPPER_NODE_ENV = "CONTEXT_MAPPER_NODE" class ContextMapperDslServer( pluginDescriptor: PluginDescriptor ) : OSProcessStreamConnectionProvider() { init { val command = - GeneralCommandLine("node", pluginDescriptor.pluginPath.resolve("lib/lsp/index.js").toString(), "--stdio") + GeneralCommandLine( + resolveNodeExecutable(), + pluginDescriptor.pluginPath.resolve("lib/lsp/index.js").toString(), + "--stdio", + ) commandLine = command } } + +internal fun resolveNodeExecutable( + environment: Map = System.getenv(), + isExecutable: (String) -> Boolean = { Files.isExecutable(Path(it)) } +): String { + environment[CONTEXT_MAPPER_NODE_ENV] + ?.takeIf { it.isNotBlank() } + ?.let { return it } + + return listOf( + "/opt/homebrew/bin/node", + "/usr/local/bin/node", + "/usr/bin/node", + ).firstOrNull(isExecutable) ?: "node" +} diff --git a/src/main/kotlin/org/contextmapper/intellij/utils/Constants.kt b/src/main/kotlin/org/contextmapper/intellij/utils/Constants.kt index 66ea6c4..46b5e44 100644 --- a/src/main/kotlin/org/contextmapper/intellij/utils/Constants.kt +++ b/src/main/kotlin/org/contextmapper/intellij/utils/Constants.kt @@ -1,4 +1,5 @@ package org.contextmapper.intellij.utils const val CONTEXT_MAPPER_SERVER_ID = "cml-language-server" +const val CONTEXT_MAPPER_LANGUAGE_ID = "context-mapper-dsl" const val PLUGIN_ID = "org.contextmapper.intellij-plugin" diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ac9ce6c..c77036b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -59,7 +59,7 @@ - + - \ No newline at end of file + diff --git a/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServerTest.kt b/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServerTest.kt new file mode 100644 index 0000000..c662669 --- /dev/null +++ b/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServerTest.kt @@ -0,0 +1,39 @@ +package org.contextmapper.intellij.lsp4ij.server + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContextMapperDslServerTest { + @Test + fun `uses explicit node executable override when configured`() { + val executable = + resolveNodeExecutable( + environment = mapOf(CONTEXT_MAPPER_NODE_ENV to "/custom/node"), + isExecutable = { false }, + ) + + assertEquals("/custom/node", executable) + } + + @Test + fun `uses common macos node location when available`() { + val executable = + resolveNodeExecutable( + environment = emptyMap(), + isExecutable = { it == "/opt/homebrew/bin/node" }, + ) + + assertEquals("/opt/homebrew/bin/node", executable) + } + + @Test + fun `falls back to path lookup when common node locations are unavailable`() { + val executable = + resolveNodeExecutable( + environment = emptyMap(), + isExecutable = { false }, + ) + + assertEquals("node", executable) + } +} diff --git a/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/PluginXmlLanguageMappingTest.kt b/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/PluginXmlLanguageMappingTest.kt new file mode 100644 index 0000000..5a1e6dc --- /dev/null +++ b/src/test/kotlin/org/contextmapper/intellij/lsp4ij/server/PluginXmlLanguageMappingTest.kt @@ -0,0 +1,28 @@ +package org.contextmapper.intellij.lsp4ij.server + +import org.contextmapper.intellij.utils.CONTEXT_MAPPER_LANGUAGE_ID +import org.contextmapper.intellij.utils.CONTEXT_MAPPER_SERVER_ID +import org.w3c.dom.Element +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class PluginXmlLanguageMappingTest { + @Test + fun `maps Context Mapper files to bundled language server language id`() { + val document = + DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(File("src/main/resources/META-INF/plugin.xml")) + + val languageMapping = + (0 until document.getElementsByTagName("languageMapping").length) + .map { document.getElementsByTagName("languageMapping").item(it) as Element } + .singleOrNull { it.getAttribute("serverId") == CONTEXT_MAPPER_SERVER_ID } + + assertNotNull(languageMapping) + assertEquals(CONTEXT_MAPPER_LANGUAGE_ID, languageMapping.getAttribute("languageId")) + } +}