From fac0145820685e358a0abc4fa2937e5c90f465a2 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 12 Mar 2026 01:40:24 +0100 Subject: [PATCH 1/4] Add DABs LSP language client for deployment-aware features Introduces a BundleLSPClient that spawns `databricks bundle lsp` via stdio to provide deployment-aware features for bundle YAML files: - Document links: Ctrl+click on resource keys to open in workspace - Hover: Shows resource ID, name, and workspace link The client connects to the CLI's new hidden LSP server and activates for all bundle YAML files (databricks.yml, databricks.yaml, etc.). Stacks with the existing Red Hat YAML extension. Depends on: databricks/cli#4714 Co-Authored-By: Claude Opus 4.6 --- packages/databricks-vscode/package.json | 1 + .../src/bundle/BundleLSPClient.ts | 93 +++++++++++++++++++ packages/databricks-vscode/src/extension.ts | 15 +++ yarn.lock | 45 +++++++++ 4 files changed, 154 insertions(+) create mode 100644 packages/databricks-vscode/src/bundle/BundleLSPClient.ts diff --git a/packages/databricks-vscode/package.json b/packages/databricks-vscode/package.json index 52b6166a3..b7918d7c0 100644 --- a/packages/databricks-vscode/package.json +++ b/packages/databricks-vscode/package.json @@ -1201,6 +1201,7 @@ "minimatch": "^10.0.1", "shell-quote": "^1.8.1", "triple-beam": "^1.4.1", + "vscode-languageclient": "^9.0.1", "winston": "^3.11.0", "yaml": "^2.3.4" }, diff --git a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts new file mode 100644 index 000000000..eb3592f13 --- /dev/null +++ b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts @@ -0,0 +1,93 @@ +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from "vscode-languageclient/node"; +import {Disposable, Uri, workspace} from "vscode"; +import {CliWrapper} from "../cli/CliWrapper"; + +/** + * Manages the lifecycle of the DABs Language Server Protocol client. + * Spawns `databricks bundle lsp` and connects via stdio to provide + * deployment-aware features (document links, hover) for bundle YAML files. + */ +export class BundleLSPClient implements Disposable { + private client: LanguageClient | undefined; + private readonly cli: CliWrapper; + + constructor(cli: CliWrapper) { + this.cli = cli; + } + + async start(workspaceFolder: Uri, target?: string): Promise { + // Stop existing client if running. + await this.stop(); + + const args = ["bundle", "lsp"]; + if (target) { + args.push("--target", target); + } + + const serverOptions: ServerOptions = { + command: this.cli.cliPath, + args: args, + options: { + cwd: workspaceFolder.fsPath, + }, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { + scheme: "file", + language: "yaml", + pattern: "**/databricks.yml", + }, + { + scheme: "file", + language: "yaml", + pattern: "**/databricks.yaml", + }, + { + scheme: "file", + language: "yaml", + pattern: "**/bundle.yml", + }, + { + scheme: "file", + language: "yaml", + pattern: "**/bundle.yaml", + }, + ], + workspaceFolder: { + uri: workspaceFolder, + name: workspace.name ?? "workspace", + index: 0, + }, + }; + + this.client = new LanguageClient( + "databricks-bundle-lsp", + "Databricks Bundle LSP", + serverOptions, + clientOptions + ); + + await this.client.start(); + } + + async stop(): Promise { + if (this.client) { + try { + await this.client.stop(); + } catch { + // Client may already be stopped. + } + this.client = undefined; + } + } + + dispose(): void { + this.stop(); + } +} diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 4713b02eb..5cc701869 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -50,6 +50,7 @@ import { BundleFileSet, registerBundleAutocompleteProvider, } from "./bundle"; +import {BundleLSPClient} from "./bundle/BundleLSPClient"; import {showWhatsNewPopup} from "./whatsNewPopup"; import {BundleValidateModel} from "./bundle/models/BundleValidateModel"; import {ConfigModel} from "./configuration/models/ConfigModel"; @@ -890,6 +891,20 @@ export async function activate( ); }); + // Start the DABs LSP client for deployment-aware features (document links, hover). + const bundleLSPClient = new BundleLSPClient(cli); + context.subscriptions.push(bundleLSPClient); + + const workspaceFolder = workspace.workspaceFolders?.[0]?.uri; + if (workspaceFolder) { + bundleLSPClient.start(workspaceFolder).catch((e) => { + logging.NamedLogger.getOrCreate(Loggers.Extension).error( + "Failed to start bundle LSP: ", + e + ); + }); + } + setDbnbCellLimits(workspaceFolderManager, connectionManager).catch((e) => { logging.NamedLogger.getOrCreate(Loggers.Extension).error( "Error while setting jupyter configs for parsing databricks notebooks", diff --git a/yarn.lock b/yarn.lock index bff6cb6c3..0189ec28c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,6 +3799,7 @@ __metadata: ts-node: ^10.9.2 typescript: ^5.3.3 vsce: ^2.15.0 + vscode-languageclient: ^9.0.1 wdio-video-reporter: ^5.1.4 wdio-vscode-service: ^6.0.2 winston: ^3.11.0 @@ -9520,6 +9521,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.7": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e + languageName: node + linkType: hard + "semver@npm:^7.5.2, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -10840,6 +10850,41 @@ __metadata: languageName: node linkType: hard +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: f302a01e59272adc1ae6494581fa31c15499f9278df76366e3b97b2236c7c53ebfc71efbace9041cfd2caa7f91675b9e56f2407871a1b3c7f760a2e2ee61484a + languageName: node + linkType: hard + +"vscode-languageclient@npm:^9.0.1": + version: 9.0.1 + resolution: "vscode-languageclient@npm:9.0.1" + dependencies: + minimatch: ^5.1.0 + semver: ^7.3.7 + vscode-languageserver-protocol: 3.17.5 + checksum: ff30e5a9aac6726a88fe443fd631fe7e6130225299667c13162547f0a13ca2548d3f277d68cfb3ef6dc1810c048b2cf0e05024e1bcbd11982cd8acf681a67873 + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + checksum: dfb42d276df5dfea728267885b99872ecff62f6c20448b8539fae71bb196b420f5351c5aca7c1047bf8fb1f89fa94a961dce2bc5bf7e726198f4be0bb86a1e71 + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 79b420e7576398d396579ca3a461c9ed70e78db4403cd28bbdf4d3ed2b66a2b4114031172e51fad49f0baa60a2180132d7cb2ea35aa3157d7af3c325528210ac + languageName: node + linkType: hard + "vscode-uri@npm:^3.0.8": version: 3.0.8 resolution: "vscode-uri@npm:3.0.8" From eeeb710294d43e10366f020fa25cab8b5ad4c9c6 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 12 Mar 2026 02:32:33 +0100 Subject: [PATCH 2/4] Update LSP command path to experimental bundle-lsp The CLI moved the LSP server from `databricks bundle lsp` to `databricks experimental bundle-lsp`. Co-Authored-By: Claude Opus 4.6 --- packages/databricks-vscode/src/bundle/BundleLSPClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts index eb3592f13..82988b9bf 100644 --- a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts +++ b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts @@ -8,7 +8,7 @@ import {CliWrapper} from "../cli/CliWrapper"; /** * Manages the lifecycle of the DABs Language Server Protocol client. - * Spawns `databricks bundle lsp` and connects via stdio to provide + * Spawns `databricks experimental bundle-lsp` and connects via stdio to provide * deployment-aware features (document links, hover) for bundle YAML files. */ export class BundleLSPClient implements Disposable { @@ -23,7 +23,7 @@ export class BundleLSPClient implements Disposable { // Stop existing client if running. await this.stop(); - const args = ["bundle", "lsp"]; + const args = ["experimental", "bundle-lsp"]; if (target) { args.push("--target", target); } @@ -68,7 +68,7 @@ export class BundleLSPClient implements Disposable { this.client = new LanguageClient( "databricks-bundle-lsp", - "Databricks Bundle LSP", + "Databricks Experimental Bundle LSP", serverOptions, clientOptions ); From f312937b15c637460033018308912ed72d9bf400 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 19 Mar 2026 14:37:42 +0100 Subject: [PATCH 3/4] Broaden document selector and add target integration for LSP - Match all .yml/.yaml files in workspace (not just root configs) so the LSP server handles included bundle files - Pass selected target to LSP server on start - Restart LSP client when user switches bundle target - Fix dispose() to return the stop() promise for clean shutdown Co-authored-by: Isaac --- .../src/bundle/BundleLSPClient.ts | 21 +++++-------- packages/databricks-vscode/src/extension.ts | 31 +++++++++++++++---- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts index 82988b9bf..dc9570e8e 100644 --- a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts +++ b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts @@ -36,27 +36,20 @@ export class BundleLSPClient implements Disposable { }, }; + // Match all YAML files in the workspace, not just the root databricks.yml. + // Bundle configs can include other YAML files via the "include" directive, + // so the LSP needs to handle any .yml/.yaml file in the project. const clientOptions: LanguageClientOptions = { documentSelector: [ { scheme: "file", language: "yaml", - pattern: "**/databricks.yml", + pattern: `${workspaceFolder.fsPath}/**/*.yml`, }, { scheme: "file", language: "yaml", - pattern: "**/databricks.yaml", - }, - { - scheme: "file", - language: "yaml", - pattern: "**/bundle.yml", - }, - { - scheme: "file", - language: "yaml", - pattern: "**/bundle.yaml", + pattern: `${workspaceFolder.fsPath}/**/*.yaml`, }, ], workspaceFolder: { @@ -87,7 +80,7 @@ export class BundleLSPClient implements Disposable { } } - dispose(): void { - this.stop(); + dispose(): Promise { + return this.stop(); } } diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 5cc701869..e0bac2ebb 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -897,12 +897,31 @@ export async function activate( const workspaceFolder = workspace.workspaceFolders?.[0]?.uri; if (workspaceFolder) { - bundleLSPClient.start(workspaceFolder).catch((e) => { - logging.NamedLogger.getOrCreate(Loggers.Extension).error( - "Failed to start bundle LSP: ", - e - ); - }); + bundleLSPClient + .start(workspaceFolder, configModel.target) + .catch((e) => { + logging.NamedLogger.getOrCreate(Loggers.Extension).error( + "Failed to start bundle LSP: ", + e + ); + }); + + // Restart the LSP client when the bundle target changes so the server + // picks up the new target's configuration. + context.subscriptions.push( + configModel.onDidChangeTarget(() => { + bundleLSPClient + .start(workspaceFolder, configModel.target) + .catch((e) => { + logging.NamedLogger.getOrCreate( + Loggers.Extension + ).error( + "Failed to restart bundle LSP after target change: ", + e + ); + }); + }) + ); } setDbnbCellLimits(workspaceFolderManager, connectionManager).catch((e) => { From 6f5801937c6012774c35e1b532d3fa63a8454dac Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 19 Mar 2026 16:43:08 +0100 Subject: [PATCH 4/4] Gate bundle LSP behind experimental flag and add error handling Gate the DABs LSP client behind the "features.bundleLsp" experimental setting to prevent failures when the CLI doesn't support the command. Add a custom error handler to suppress crash notification popups and prevent restart storms when the server binary exits immediately. Co-authored-by: Isaac --- packages/databricks-vscode/package.json | 6 +- .../src/bundle/BundleLSPClient.ts | 13 ++++ packages/databricks-vscode/src/extension.ts | 70 +++++++++++-------- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/packages/databricks-vscode/package.json b/packages/databricks-vscode/package.json index b7918d7c0..4aa3c2ba1 100644 --- a/packages/databricks-vscode/package.json +++ b/packages/databricks-vscode/package.json @@ -1105,11 +1105,13 @@ "items": { "enum": [ "views.cluster", - "views.workspace" + "views.workspace", + "features.bundleLsp" ], "enumDescriptions": [ "Show cluster view in the explorer.", - "Show workspace browser in the explorer." + "Show workspace browser in the explorer.", + "Enable the DABs Language Server for bundle YAML files (autocomplete, go-to-definition, diagnostics)." ], "type": "string" }, diff --git a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts index dc9570e8e..baa112e9a 100644 --- a/packages/databricks-vscode/src/bundle/BundleLSPClient.ts +++ b/packages/databricks-vscode/src/bundle/BundleLSPClient.ts @@ -1,6 +1,9 @@ import { + CloseAction, + ErrorAction, LanguageClient, LanguageClientOptions, + RevealOutputChannelOn, ServerOptions, } from "vscode-languageclient/node"; import {Disposable, Uri, workspace} from "vscode"; @@ -57,6 +60,16 @@ export class BundleLSPClient implements Disposable { name: workspace.name ?? "workspace", index: 0, }, + // Suppress error popups if the server binary doesn't support + // the bundle-lsp command (e.g. older CLI versions). + revealOutputChannelOn: RevealOutputChannelOn.Never, + errorHandler: { + error: () => ({action: ErrorAction.Shutdown}), + closed: () => ({ + action: CloseAction.DoNotRestart, + handled: true, + }), + }, }; this.client = new LanguageClient( diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index e0bac2ebb..07639f08f 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -891,37 +891,45 @@ export async function activate( ); }); - // Start the DABs LSP client for deployment-aware features (document links, hover). - const bundleLSPClient = new BundleLSPClient(cli); - context.subscriptions.push(bundleLSPClient); - - const workspaceFolder = workspace.workspaceFolders?.[0]?.uri; - if (workspaceFolder) { - bundleLSPClient - .start(workspaceFolder, configModel.target) - .catch((e) => { - logging.NamedLogger.getOrCreate(Loggers.Extension).error( - "Failed to start bundle LSP: ", - e - ); - }); - - // Restart the LSP client when the bundle target changes so the server - // picks up the new target's configuration. - context.subscriptions.push( - configModel.onDidChangeTarget(() => { - bundleLSPClient - .start(workspaceFolder, configModel.target) - .catch((e) => { - logging.NamedLogger.getOrCreate( - Loggers.Extension - ).error( - "Failed to restart bundle LSP after target change: ", - e - ); - }); - }) - ); + // Start the DABs LSP client for deployment-aware features + // (autocomplete, go-to-definition, hover, diagnostics). + // Gated behind the "features.bundleLsp" experimental flag. + if ( + workspaceConfigs.experimetalFeatureOverides.includes( + "features.bundleLsp" + ) + ) { + const bundleLSPClient = new BundleLSPClient(cli); + context.subscriptions.push(bundleLSPClient); + + const workspaceFolder = workspace.workspaceFolders?.[0]?.uri; + if (workspaceFolder) { + bundleLSPClient + .start(workspaceFolder, configModel.target) + .catch((e) => { + logging.NamedLogger.getOrCreate(Loggers.Extension).error( + "Failed to start bundle LSP: ", + e + ); + }); + + // Restart the LSP client when the bundle target changes so the server + // picks up the new target's configuration. + context.subscriptions.push( + configModel.onDidChangeTarget(() => { + bundleLSPClient + .start(workspaceFolder, configModel.target) + .catch((e) => { + logging.NamedLogger.getOrCreate( + Loggers.Extension + ).error( + "Failed to restart bundle LSP after target change: ", + e + ); + }); + }) + ); + } } setDbnbCellLimits(workspaceFolderManager, connectionManager).catch((e) => {