Skip to content

Commit 389500c

Browse files
authored
Merge pull request #45 from skurlow/codex/context-mapper-plugin-setup-fixes
Fix IntelliJ 2026.1 compatibility and Context Mapper setup
2 parents 9c8b297 + fdd1f25 commit 389500c

10 files changed

Lines changed: 204 additions & 6 deletions

File tree

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,97 @@ To use the Context Mapper IntelliJ plugin you need the following tools (besides
1414
* [Oracle Java](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) or [OpenJDK](https://openjdk.java.net/) (JRE 11 or newer)
1515
* [Node.js](https://nodejs.org/en/download) (v22)
1616
* Maybe you want to install a [PlantUML extension](https://plugins.jetbrains.com/plugin/7017-plantuml-integration) for the generated PlantUML diagrams.
17+
* [Graphviz](https://graphviz.org/) if you want to render generated PlantUML diagrams in IntelliJ.
1718
* LSP4IJ IntelliJ plugin (will be installed automatically when installing our plugin)
1819

1920
<!-- Plugin description end -->
2021

22+
## Getting Started
23+
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.
24+
25+
### 1. Install the IntelliJ plugins
26+
Install the Context Mapper plugin ZIP in IntelliJ:
27+
28+
1. Open **Settings > Plugins**.
29+
2. Click the gear icon and choose **Install Plugin from Disk...**.
30+
3. Select the built plugin ZIP from `build/distributions/context-mapper-intellij-plugin.zip`.
31+
4. Restart IntelliJ when prompted.
32+
33+
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**.
34+
35+
### 2. Install local tools
36+
The Context Mapper language server is a Node.js process. Make sure `node` is installed and visible to IntelliJ:
37+
38+
```bash
39+
node --version
40+
which node
41+
```
42+
43+
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.
44+
45+
To preview generated PlantUML diagrams inside IntelliJ, install the PlantUML Integration plugin and Graphviz:
46+
47+
```bash
48+
brew install graphviz
49+
which dot
50+
dot -V
51+
```
52+
53+
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.
54+
55+
### 3. Create a simple CML file
56+
Create a file such as `architecture/context-maps/example.cml`:
57+
58+
```cml
59+
ContextMap ExampleMap {
60+
contains OrderingContext
61+
contains BillingContext
62+
63+
OrderingContext [SK]<->[SK] BillingContext
64+
}
65+
66+
BoundedContext OrderingContext {
67+
type = APPLICATION
68+
domainVisionStatement = "Handles customer order placement."
69+
}
70+
71+
BoundedContext BillingContext {
72+
type = APPLICATION
73+
domainVisionStatement = "Handles invoicing and payment workflows."
74+
}
75+
```
76+
77+
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.
78+
79+
### 4. Generate a PlantUML diagram
80+
Open the `.cml` file in the editor, then run:
81+
82+
1. Right-click inside the editor.
83+
2. Choose **Context Mapper > Generate PlantUML Diagrams**.
84+
85+
You can also search for the action with **Cmd+Shift+A** and run **Generate PlantUML Diagrams**.
86+
87+
The generated file is written to:
88+
89+
```text
90+
<project-root>/src-gen/<source-file-name>.puml
91+
```
92+
93+
For example:
94+
95+
```text
96+
src-gen/example.puml
97+
```
98+
99+
Open that `.puml` file to preview it with the PlantUML plugin.
100+
101+
### Troubleshooting
102+
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`.
103+
104+
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.
105+
106+
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`.
107+
21108
## Build and/or Run Extension Locally
22109
This project uses [Gradle](https://gradle.org/) to build the IntelliJ plugin.
23110

@@ -41,6 +128,18 @@ After cloning this repository, you can build the project with the following comm
41128
./gradlew clean buildPlugin
42129
```
43130

131+
Before installing a built plugin ZIP into a newer IntelliJ IDEA version, verify plugin compatibility:
132+
133+
```bash
134+
./gradlew verifyPlugin
135+
```
136+
137+
The verifier target defaults to IntelliJ IDEA Ultimate 2026.1.3. Override it when needed:
138+
139+
```bash
140+
./gradlew verifyPlugin -PintellijVerifierIdeVersion=<IDE_VERSION>
141+
```
142+
44143
Use the following command to build and run the plugin:
45144

46145
```bash

build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ intellijPlatform {
8484
publishing {
8585
token = System.getenv("PUBLISH_TOKEN")
8686
}
87+
88+
pluginVerification {
89+
ides {
90+
ide(providers.provider { "IU" }, providers.gradleProperty("intellijVerifierIdeVersion").orElse("2026.1.3"))
91+
}
92+
}
8793
}
8894

8995
kotlin {

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ org.gradle.caching=true
77

88
intelliJVersion=2024.2.6
99
ideaSinceBuild=241
10-
ideaUntilBuild=251.*
10+
ideaUntilBuild=261.*
1111

1212
cmlVersion=6.12.0
1313
lsp4ijVersion=0.13.0
1414
mockkVersion=1.13.17
15-
archUnitVersion=1.4.1
15+
archUnitVersion=1.4.1
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
#Tue Jun 16 16:37:07 AEST 2026
12
distributionBase=GRADLE_USER_HOME
23
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
45
zipStoreBase=GRADLE_USER_HOME
56
zipStorePath=wrapper/dists

lsp/.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@contextmapper:registry=https://npm.pkg.github.com

src/main/kotlin/org/contextmapper/intellij/lsp4ij/server/ContextMapperDslServer.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,36 @@ package org.contextmapper.intellij.lsp4ij.server
33
import com.intellij.execution.configurations.GeneralCommandLine
44
import com.intellij.openapi.extensions.PluginDescriptor
55
import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider
6+
import java.nio.file.Files
7+
import kotlin.io.path.Path
8+
9+
internal const val CONTEXT_MAPPER_NODE_ENV = "CONTEXT_MAPPER_NODE"
610

711
class ContextMapperDslServer(
812
pluginDescriptor: PluginDescriptor
913
) : OSProcessStreamConnectionProvider() {
1014
init {
1115
val command =
12-
GeneralCommandLine("node", pluginDescriptor.pluginPath.resolve("lib/lsp/index.js").toString(), "--stdio")
16+
GeneralCommandLine(
17+
resolveNodeExecutable(),
18+
pluginDescriptor.pluginPath.resolve("lib/lsp/index.js").toString(),
19+
"--stdio",
20+
)
1321
commandLine = command
1422
}
1523
}
24+
25+
internal fun resolveNodeExecutable(
26+
environment: Map<String, String> = System.getenv(),
27+
isExecutable: (String) -> Boolean = { Files.isExecutable(Path(it)) }
28+
): String {
29+
environment[CONTEXT_MAPPER_NODE_ENV]
30+
?.takeIf { it.isNotBlank() }
31+
?.let { return it }
32+
33+
return listOf(
34+
"/opt/homebrew/bin/node",
35+
"/usr/local/bin/node",
36+
"/usr/bin/node",
37+
).firstOrNull(isExecutable) ?: "node"
38+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package org.contextmapper.intellij.utils
22

33
const val CONTEXT_MAPPER_SERVER_ID = "cml-language-server"
4+
const val CONTEXT_MAPPER_LANGUAGE_ID = "context-mapper-dsl"
45
const val PLUGIN_ID = "org.contextmapper.intellij-plugin"

src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</description>
6060
</server>
6161

62-
<languageMapping language="Context Mapper DSL" serverId="cml-language-server" languageId="Context Mapper DSL"/>
62+
<languageMapping language="Context Mapper DSL" serverId="cml-language-server" languageId="context-mapper-dsl"/>
6363

6464
<semanticTokensColorsProvider serverId="cml-language-server"
6565
class="org.contextmapper.intellij.lsp4ij.syntaxhighlighting.ContextMapperDSLSemanticTokensColorProvider"
@@ -82,4 +82,4 @@
8282
/>
8383
</group>
8484
</actions>
85-
</idea-plugin>
85+
</idea-plugin>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.contextmapper.intellij.lsp4ij.server
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
6+
class ContextMapperDslServerTest {
7+
@Test
8+
fun `uses explicit node executable override when configured`() {
9+
val executable =
10+
resolveNodeExecutable(
11+
environment = mapOf(CONTEXT_MAPPER_NODE_ENV to "/custom/node"),
12+
isExecutable = { false },
13+
)
14+
15+
assertEquals("/custom/node", executable)
16+
}
17+
18+
@Test
19+
fun `uses common macos node location when available`() {
20+
val executable =
21+
resolveNodeExecutable(
22+
environment = emptyMap(),
23+
isExecutable = { it == "/opt/homebrew/bin/node" },
24+
)
25+
26+
assertEquals("/opt/homebrew/bin/node", executable)
27+
}
28+
29+
@Test
30+
fun `falls back to path lookup when common node locations are unavailable`() {
31+
val executable =
32+
resolveNodeExecutable(
33+
environment = emptyMap(),
34+
isExecutable = { false },
35+
)
36+
37+
assertEquals("node", executable)
38+
}
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.contextmapper.intellij.lsp4ij.server
2+
3+
import org.contextmapper.intellij.utils.CONTEXT_MAPPER_LANGUAGE_ID
4+
import org.contextmapper.intellij.utils.CONTEXT_MAPPER_SERVER_ID
5+
import org.w3c.dom.Element
6+
import java.io.File
7+
import javax.xml.parsers.DocumentBuilderFactory
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNotNull
11+
12+
class PluginXmlLanguageMappingTest {
13+
@Test
14+
fun `maps Context Mapper files to bundled language server language id`() {
15+
val document =
16+
DocumentBuilderFactory.newInstance()
17+
.newDocumentBuilder()
18+
.parse(File("src/main/resources/META-INF/plugin.xml"))
19+
20+
val languageMapping =
21+
(0 until document.getElementsByTagName("languageMapping").length)
22+
.map { document.getElementsByTagName("languageMapping").item(it) as Element }
23+
.singleOrNull { it.getAttribute("serverId") == CONTEXT_MAPPER_SERVER_ID }
24+
25+
assertNotNull(languageMapping)
26+
assertEquals(CONTEXT_MAPPER_LANGUAGE_ID, languageMapping.getAttribute("languageId"))
27+
}
28+
}

0 commit comments

Comments
 (0)