Skip to content

Commit d352f02

Browse files
authored
fix(jetbrains): implement dynamic versioning (#2439)
Support switching versions based on generator.version, and correctly handle the case where the latest version gets bumped. cli_downloader is only lightly reviewed, it's mostly a claude-generated translation of the vscode implementation <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Implements dynamic versioning for JetBrains plugin with CLI downloader and version management, updating language server setup and adding tests. > > - **Behavior**: > - Implements dynamic versioning for JetBrains plugin using `CliDownloader` and `BamlLanguageServerService`. > - Handles version switching via `generatorVersionNotification()` in `BamlLanguageClient.kt`. > - Downloads and verifies CLI binaries using `CliDownloader` and `ChecksumVerifier`. > - **Language Server**: > - Updates `BamlLanguageServer` to use dynamic CLI paths. > - Adds `BamlLanguageServerFactory` and `BamlLanguageServerInstaller` for server management. > - **Configuration**: > - Adds `DownloadConfig`, `TimeoutConfig`, and `BackoffConfig` for managing download settings. > - Introduces `PlatformDetector` and `PlatformMapper` for platform-specific operations. > - **Testing**: > - Adds unit tests for `CliVersion`, `DownloadConfig`, `PlatformDetector`, and `PlatformMapper`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for d3354a1. You can [customize](https://app.ellipsis.dev/BoundaryML/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent 7739eac commit d352f02

31 files changed

Lines changed: 1293 additions & 349 deletions

engine/language_server/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![allow(dead_code)]
2-
31
use std::num::NonZeroUsize;
42

53
use anyhow::Context;

engine/playground-server/src/server.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ async fn playground_static_assets() -> anyhow::Result<PathBuf> {
5858
const GITHUB_REPO: &str = "BoundaryML/baml";
5959

6060
if std::env::var("VSCODE_DEBUG_MODE")
61-
.map(|v| v == "true")
62-
.unwrap_or(false)
61+
.is_ok_and(|v| v.to_lowercase() == "true" || v.to_lowercase() == "1")
6362
{
6463
// Use cargo-relative path for local dist
6564
let local_dist = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))

engine/zed/src/lib.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ struct BamlBinary {
1616
args: Option<Vec<String>>,
1717
}
1818

19-
struct BamlExtension {
20-
cached_binary_path: Option<String>,
21-
}
19+
struct BamlExtension {}
2220

2321
impl BamlExtension {
2422
fn language_server_binary(
@@ -145,9 +143,7 @@ impl BamlExtension {
145143

146144
impl zed::Extension for BamlExtension {
147145
fn new() -> Self {
148-
Self {
149-
cached_binary_path: None,
150-
}
146+
Self {}
151147
}
152148

153149
fn language_server_command(
@@ -157,11 +153,11 @@ impl zed::Extension for BamlExtension {
157153
) -> Result<zed::Command> {
158154
let baml_binary = self.language_server_binary(language_server_id, worktree)?;
159155
Ok(zed::Command {
160-
// command: baml_binary.path,
161-
command: format!(
162-
"{}/../target/debug/language-server-hot-reload",
163-
env!("CARGO_MANIFEST_DIR")
164-
),
156+
command: baml_binary.path,
157+
// command: format!(
158+
// "{}/../target/debug/language-server-hot-reload",
159+
// env!("CARGO_MANIFEST_DIR")
160+
// ),
165161
args: baml_binary.args.unwrap_or_else(|| vec!["lsp".into()]),
166162
env: Default::default(),
167163
})

jetbrains/.run/Run Plugin in IDE.run.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<ExternalSystemSettings>
55
<option name="env">
66
<map>
7-
<entry key="VSCODE_DEBUG_MODE" value="true" />
87
<entry key="JETBRAINS_PROJECT_DIR" value="$PROJECT_DIR$" />
8+
<entry key="VSCODE_DEBUG_MODE" value="true" />
99
</map>
1010
</option>
1111
<option name="executionName" />
@@ -20,7 +20,6 @@
2020
<option value="runIde" />
2121
</list>
2222
</option>
23-
<option name="vmOptions" value="-Didea.log.console.stdout.level=DEBUG" />
2423
</ExternalSystemSettings>
2524
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
2625
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>

jetbrains/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ dependencies {
4949
}
5050

5151
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
52+
53+
// CLI Downloader dependencies
54+
implementation("com.squareup.okhttp3:okhttp:4.12.0")
55+
implementation("org.apache.commons:commons-compress:1.24.0")
56+
implementation("org.slf4j:slf4j-api:2.0.9")
57+
implementation("ch.qos.logback:logback-classic:1.4.11")
58+
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
59+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
5260
}
5361

5462
// Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html

jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlCustomServerAPI.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.

jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlGetPortService.kt

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.boundaryml.jetbrains_ext
22

33
object BamlIdeConfig {
4-
val isDebugMode: Boolean
5-
4+
private val isDebugMode: Boolean
5+
66
init {
77
val debugModeEnv = System.getenv("VSCODE_DEBUG_MODE")
88
isDebugMode = debugModeEnv == "true"
99
println("BamlIdeConfig: VSCODE_DEBUG_MODE=${debugModeEnv ?: "(unset)"}, isDebugMode=$isDebugMode")
1010
}
11-
11+
1212
fun getPlaygroundUrl(port: Int): String {
1313
return "http://localhost:$port/"
1414
}
15+
16+
fun shouldShowToolWindowDebuggers(): Boolean = isDebugMode
17+
fun shouldUseLocalLanguageServerBuild(): Boolean = false
1518
}

jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package com.boundaryml.jetbrains_ext
2+
23
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
34
import com.intellij.lang.Language
45

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,147 @@
11
package com.boundaryml.jetbrains_ext
22

3-
import BamlCustomServerAPI
4-
import PortParams
3+
import com.boundaryml.jetbrains_ext.cli_downloader.CliVersion
4+
import com.intellij.notification.NotificationGroupManager
5+
import com.intellij.notification.NotificationType
6+
import com.intellij.openapi.application.ApplicationManager
7+
import com.intellij.openapi.components.service
58
import com.intellij.openapi.diagnostic.Logger
69
import com.intellij.openapi.project.Project
10+
import com.redhat.devtools.lsp4ij.LanguageServerManager
11+
import com.redhat.devtools.lsp4ij.LanguageServerManager.StartOptions
12+
import com.redhat.devtools.lsp4ij.LanguageServerManager.StopOptions
13+
import com.redhat.devtools.lsp4ij.ServerStatus
714
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
15+
import com.redhat.devtools.lsp4ij.installation.ServerInstallationContext
16+
import com.redhat.devtools.lsp4ij.installation.ServerInstallationStatus
17+
import kotlinx.coroutines.runBlocking
18+
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
19+
import java.nio.file.Paths
20+
21+
22+
// Existing data class (keep as-is)
23+
data class PortParams(val port: Int)
24+
25+
// New data class for version switching
26+
data class GeneratorVersionPayload(
27+
val version: String,
28+
val root_path: String
29+
)
830

931
class BamlLanguageClient(project: Project) :
10-
LanguageClientImpl(project), BamlCustomServerAPI {
32+
LanguageClientImpl(project) {
33+
34+
private val log = Logger.getInstance(javaClass)
35+
private val languageServerService = service<BamlLanguageServerService>()
36+
37+
// NB(sam): if we need to do something after language server startup, we can apply that hook here
38+
// override fun handleServerStatusChanged(serverStatus: ServerStatus) {
39+
// super.handleServerStatusChanged(serverStatus)
40+
// }
41+
42+
// Existing port notification (keep exactly as-is but use new service)
43+
@JsonNotification("baml/port")
44+
fun onPort(params: PortParams) {
45+
log.info("Port params: ${params.port}")
46+
47+
log.info("Setting port to ${params.port}")
48+
languageServerService.setPort(params.port)
49+
log.info("Set port to ${params.port}")
50+
}
51+
52+
// Phase 2: Full version switching notification processing
53+
@JsonNotification("baml_src_generator_version")
54+
fun generatorVersionNotification(payload: GeneratorVersionPayload) {
55+
log.info("🔄 language server requested that we run a different version: $payload")
56+
57+
// Process in background to avoid blocking LSP communication
58+
ApplicationManager.getApplication().executeOnPooledThread {
59+
processVersionSwitchRequest(payload)
60+
}
61+
}
1162

12-
private val log = Logger.getInstance(BamlLanguageClient::class.java)
63+
private fun processVersionSwitchRequest(payload: GeneratorVersionPayload) {
64+
if (BamlIdeConfig.shouldUseLocalLanguageServerBuild()) {
65+
log.info("Running in development mode, ignoring version switch request")
66+
return
67+
}
1368

14-
override fun onPort(params: PortParams) {
15-
Logger.getInstance(javaClass).warn("Port params: ${params.port}")
69+
// 1. Validate notification is for current project (equivalent to VSCode's isPathWithinParent)
70+
if (!isNotificationForCurrentProject(payload.root_path)) {
71+
log.debug("Ignoring version notification for different project: ${payload.root_path}")
72+
return
73+
}
74+
75+
// 2. Check if restart already in progress (equivalent to VSCode's isRestarting flag)
76+
if (languageServerService.isCurrentlyRestarting()) {
77+
log.info("Language server restart already in progress, ignoring request")
78+
return
79+
}
80+
81+
// 3. Validate semantic version (equivalent to VSCode's semver.valid check)
82+
if (!isValidSemanticVersion(payload.version)) {
83+
log.warn("Invalid semantic version received: ${payload.version}")
84+
return
85+
}
86+
87+
// 4. Check minimum version requirement (equivalent to VSCode's >= 0.86.0 check)
88+
if (!isMinimumVersionSupported(payload.version)) {
89+
log.warn("Ignoring version ${payload.version} - below minimum supported version")
90+
return
91+
}
92+
93+
// 5. Resolve target CLI path (equivalent to VSCode's resolveCliPath call)
94+
runBlocking {
95+
// 6. Check if restart is needed (equivalent to VSCode's path comparison)
96+
if (languageServerService.getCurrentCliVersion() != payload.version) {
97+
// Update version tracking even if no restart needed
98+
languageServerService.updateCurrentServer(payload.version)
99+
// 7. Execute restart (equivalent to VSCode's executeLanguageServerRestart)
100+
log.info("Restarting language server with new version")
101+
service<BamlLanguageServerService>().setRestartingFlag(true)
102+
// https://github.com/redhat-developer/lsp4ij/blob/main/docs/DeveloperGuide.md#install-language-server
103+
// Stops the language server if it is currently starting or already started.
104+
//Resets the installer's internal state.
105+
//Executes the installation via checkInstallation(context).
106+
//If the server was previously running, it restarts it once the installation completes.
107+
val context = ServerInstallationContext()
108+
.setForceInstall(true)
109+
LanguageServerManager.getInstance(project)
110+
.install("baml-language-server", context)
111+
}
112+
log.info("Already using correct CLI version, no restart needed")
113+
114+
}
115+
}
116+
117+
private fun isNotificationForCurrentProject(rootPath: String): Boolean {
118+
val projectBasePath = project.basePath ?: return false
119+
return try {
120+
val notificationPath = Paths.get(rootPath).normalize()
121+
val projectPath = Paths.get(projectBasePath).normalize()
122+
// Check if paths overlap (either direction)
123+
notificationPath.startsWith(projectPath) || projectPath.startsWith(notificationPath)
124+
} catch (e: Exception) {
125+
log.warn("Error validating project path: $rootPath", e)
126+
false
127+
}
128+
}
129+
130+
private fun isValidSemanticVersion(version: String): Boolean {
131+
// Basic semantic version validation (x.y.z pattern)
132+
return version.matches(Regex("\\d+\\.\\d+\\.\\d+.*"))
133+
}
16134

17-
System.out.println("Setting port to ${params.port}")
18-
project.getService(BamlGetPortService::class.java)
19-
.setPort(params.port)
20-
System.out.println("Set port to ${params.port}")
135+
private fun isMinimumVersionSupported(version: String): Boolean {
136+
// Only versions 0.86.0+ support this notification (like VSCode)
137+
return try {
138+
val versionParts = version.split(".")
139+
if (versionParts.size < 3) return false
140+
val major = versionParts[0].toInt()
141+
val minor = versionParts[1].toInt()
142+
major > 0 || (major == 0 && minor >= 86)
143+
} catch (e: Exception) {
144+
false
145+
}
21146
}
22147
}

0 commit comments

Comments
 (0)