From 666982b34d76ed1b6f8291dbae45d1209c62fc99 Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Thu, 12 Feb 2026 10:30:59 +0100 Subject: [PATCH 1/8] dbeaver/pro#8065 feat: add LSP client support and integrate into SQL editor --- .../packages/plugin-codemirror6/package.json | 1 + .../packages/plugin-codemirror6/src/index.ts | 3 +- .../plugin-sql-editor-codemirror/package.json | 1 + .../plugin-sql-editor-codemirror/src/index.ts | 1 + .../src/useLSPExtension.ts | 105 ++++++++++++++++++ .../tsconfig.json | 3 + .../SQLCodeEditorPanel/SQLCodeEditorPanel.tsx | 10 ++ webapp/yarn.lock | 70 +++++++++++- 8 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts diff --git a/webapp/packages/plugin-codemirror6/package.json b/webapp/packages/plugin-codemirror6/package.json index 3c429143eb0..37dcf95297f 100644 --- a/webapp/packages/plugin-codemirror6/package.json +++ b/webapp/packages/plugin-codemirror6/package.json @@ -35,6 +35,7 @@ "@codemirror/lang-sql": "^6", "@codemirror/lang-xml": "^6", "@codemirror/language": "^6", + "@codemirror/lsp-client": "^6", "@codemirror/merge": "^6", "@codemirror/search": "^6", "@codemirror/state": "^6", diff --git a/webapp/packages/plugin-codemirror6/src/index.ts b/webapp/packages/plugin-codemirror6/src/index.ts index 7a124789bc7..7bc3b5a5a7e 100644 --- a/webapp/packages/plugin-codemirror6/src/index.ts +++ b/webapp/packages/plugin-codemirror6/src/index.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ export * from './Hyperlink/useHyperlink.js'; export * from '@codemirror/view'; export * from '@codemirror/state'; export * from '@codemirror/autocomplete'; +export * from '@codemirror/lsp-client'; export * from './highlightNewLine.js'; export { html as HTML_EDITOR } from '@codemirror/lang-html'; diff --git a/webapp/packages/plugin-sql-editor-codemirror/package.json b/webapp/packages/plugin-sql-editor-codemirror/package.json index b4e6cfdbd31..8eacfb41229 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/package.json +++ b/webapp/packages/plugin-sql-editor-codemirror/package.json @@ -24,6 +24,7 @@ "dependencies": { "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", "@cloudbeaver/plugin-codemirror6": "workspace:*", "mobx": "^6", "mobx-react-lite": "^4", diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/index.ts b/webapp/packages/plugin-sql-editor-codemirror/src/index.ts index 51bdac8811c..594027ce277 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/index.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/index.ts @@ -9,5 +9,6 @@ export * from './SQLCodeEditor/SQLCodeEditorLoader.js'; export * from './SQLCodeEditor/useSQLCodeEditor.js'; export * from './useSqlDialectExtension.js'; +export * from './useLSPExtension.js'; export * from './ACTIVE_QUERY_EXTENSION.js'; export * from './QUERY_STATUS_GUTTER_EXTENSION.js'; diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts new file mode 100644 index 00000000000..041a0405216 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts @@ -0,0 +1,105 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { useMemo } from 'react'; + +import { createLazyLoader, useLazyImport } from '@cloudbeaver/core-blocks'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; +import { type Compartment, type Extension, type Transport, LSPClient, languageServerExtensions } from '@cloudbeaver/plugin-codemirror6'; + +const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6')); + +const LSP_ENDPOINT = 'ws/lsp'; + +function simpleWebSocketTransport(uri: string): Promise { + let handlers: ((value: string) => void)[] = []; + const sock = new WebSocket(uri); + + sock.onmessage = e => { + handlers.forEach(h => h(e.data as string)); + }; + + sock.onerror = e => { + console.error('[LSP] WebSocket error:', e); + }; + + sock.onclose = () => { + console.log('[LSP] WebSocket connection closed'); + }; + + return new Promise((resolve, reject) => { + sock.onopen = () => { + console.log('[LSP] WebSocket connection established'); + resolve({ + send(message: string) { + sock.send(message); + }, + subscribe(handler) { + handlers.push(handler); + }, + unsubscribe(handler) { + handlers = handlers.filter(h => h !== handler); + }, + }); + }; + + sock.onerror = e => { + console.error('[LSP] Failed to connect to WebSocket:', e); + reject(e); + }; + }); +} + +export interface ILSPExtensionOptions { + projectId: string | null | undefined; + resourcePath: string | null | undefined; +} + +export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Extension] | null { + const { projectId, resourcePath } = options; + const codemirror = useLazyImport(codemirrorPluginLoader); + + const LSP_COMPARTMENT = useMemo(() => { + if (!codemirror) { + return null; + } + return new codemirror.Compartment(); + }, [codemirror]); + + const client = useMemo(() => { + const lspClient = new LSPClient({ + extensions: languageServerExtensions(), + }); + + const lspServerUrl = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT); + + simpleWebSocketTransport(lspServerUrl) + .then(transport => { + lspClient.connect(transport); + }) + .catch(error => { + console.error('[LSP] Failed to initialize LSP client:', error); + }); + + return lspClient; + }, []); + + const documentUri = useMemo(() => { + if (!projectId || !resourcePath) { + return null; + } + return `lsp://${projectId}/${resourcePath}`; + }, [projectId, resourcePath]); + + return useMemo(() => { + if (!LSP_COMPARTMENT || !client || !codemirror || !documentUri) { + return null; + } + + return [LSP_COMPARTMENT, client.plugin(documentUri)]; + }, [LSP_COMPARTMENT, client, codemirror, documentUri]); +} diff --git a/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json b/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json index 3072dc9d943..abff46a96ea 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json @@ -16,6 +16,9 @@ { "path": "../core-sdk" }, + { + "path": "../core-utils" + }, { "path": "../plugin-codemirror6" } diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx index 0e70e575abe..e16fa3b113b 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx @@ -22,6 +22,7 @@ import { SQLCodeEditor, useSQLCodeEditor, useSqlDialectExtension, + useLSPExtension, } from '@cloudbeaver/plugin-sql-editor-codemirror'; import { useHighlightExtensions } from '../useHighlightExtensions.js'; import { useSqlDialectAutocompletion } from '../useSqlDialectAutocompletion.js'; @@ -46,6 +47,11 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent const sqlDialect = useSqlDialectExtension(data.dialect); const highlightExtensions = useHighlightExtensions(sqlEditorSettingsService.highlightWhitespace); + const lspExtension = useLSPExtension({ + projectId: data.model.dataSource?.projectId, + resourcePath: data.model.state?.editorId, + }); + if (autocompletion) { extensions.set(...autocompletion); } @@ -60,6 +66,10 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent }); } + if (lspExtension) { + extensions.set(...lspExtension); + } + const dndBox = useDNDBox({ canDrop: context => context.has(DATA_CONTEXT_NAV_NODE), onDrop: async (context, mouse) => { diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 9e51115dbd3..1ef1ae63df4 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2595,6 +2595,7 @@ __metadata: "@codemirror/lang-sql": "npm:^6" "@codemirror/lang-xml": "npm:^6" "@codemirror/language": "npm:^6" + "@codemirror/lsp-client": "npm:^6" "@codemirror/merge": "npm:^6" "@codemirror/search": "npm:^6" "@codemirror/state": "npm:^6" @@ -4115,6 +4116,7 @@ __metadata: "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" "@cloudbeaver/core-sdk": "workspace:*" + "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/plugin-codemirror6": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@types/react": "npm:^19" @@ -4733,7 +4735,7 @@ __metadata: languageName: unknown linkType: soft -"@codemirror/autocomplete@npm:^6, @codemirror/autocomplete@npm:^6.0.0": +"@codemirror/autocomplete@npm:^6, @codemirror/autocomplete@npm:^6.0.0, @codemirror/autocomplete@npm:^6.20.0": version: 6.20.0 resolution: "@codemirror/autocomplete@npm:6.20.0" dependencies: @@ -4840,7 +4842,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/language@npm:^6, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": +"@codemirror/language@npm:^6, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.11.0, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": version: 6.12.1 resolution: "@codemirror/language@npm:6.12.1" dependencies: @@ -4865,6 +4867,33 @@ __metadata: languageName: node linkType: hard +"@codemirror/lint@npm:^6.8.5": + version: 6.9.4 + resolution: "@codemirror/lint@npm:6.9.4" + dependencies: + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.35.0" + crelt: "npm:^1.0.5" + checksum: 10c0/ae11d4d61250010732b10e4d392bd987c78afbc06ea4ccc351b57050bb5ffc74a866e5efdd7b05684d899330d4913c81b88fb7b4a0ff9762f689722ec889c7aa + languageName: node + linkType: hard + +"@codemirror/lsp-client@npm:^6": + version: 6.2.1 + resolution: "@codemirror/lsp-client@npm:6.2.1" + dependencies: + "@codemirror/autocomplete": "npm:^6.20.0" + "@codemirror/language": "npm:^6.11.0" + "@codemirror/lint": "npm:^6.8.5" + "@codemirror/state": "npm:^6.5.2" + "@codemirror/view": "npm:^6.37.0" + "@lezer/highlight": "npm:^1.2.1" + marked: "npm:^15.0.12" + vscode-languageserver-protocol: "npm:^3.17.5" + checksum: 10c0/1e05cc5458d43d5f6ae669b8a21b4d0234428df3f99680c00e97a1aad81bf67553a1b431237b8927f687f21ae01ff79e6bd72e3202a38a4f7f2ef9b94aa97aee + languageName: node + linkType: hard + "@codemirror/merge@npm:^6": version: 6.11.2 resolution: "@codemirror/merge@npm:6.11.2" @@ -4889,7 +4918,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/state@npm:^6, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": +"@codemirror/state@npm:^6, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0, @codemirror/state@npm:^6.5.2": version: 6.5.4 resolution: "@codemirror/state@npm:6.5.4" dependencies: @@ -6663,7 +6692,7 @@ __metadata: languageName: node linkType: hard -"@lezer/highlight@npm:^1, @lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.1.3": +"@lezer/highlight@npm:^1, @lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.1.3, @lezer/highlight@npm:^1.2.1": version: 1.2.3 resolution: "@lezer/highlight@npm:1.2.3" dependencies: @@ -14788,6 +14817,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^15.0.12": + version: 15.0.12 + resolution: "marked@npm:15.0.12" + bin: + marked: bin/marked.js + checksum: 10c0/e09da211544b787ecfb25fed07af206060bf7cd6d9de6cb123f15c496a57f83b7aabea93340aaa94dae9c94e097ae129377cad6310abc16009590972e85f4212 + languageName: node + linkType: hard + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -19644,6 +19682,30 @@ __metadata: languageName: node linkType: hard +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + 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: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 10c0/1e1260de79a2cc8de3e46f2e0182cdc94a7eddab487db5a3bd4ee716f67728e685852707d72c059721ce500447be9a46764a04f0611e94e4321ffa088eef36f8 + languageName: node + linkType: hard + "w3c-keyname@npm:^2.2.4": version: 2.2.8 resolution: "w3c-keyname@npm:2.2.8" From 5db15936f7d646147a763bdc33a6f89b41c24024 Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Thu, 12 Feb 2026 14:18:26 +0100 Subject: [PATCH 2/8] dbeaver/pro#8065 feat: implement reconnecting WebSocket transport for LSP client --- .../src/useLSPExtension.ts | 118 ++++++++++++------ 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts index 041a0405216..c7be6f7d055 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createLazyLoader, useLazyImport } from '@cloudbeaver/core-blocks'; import { GlobalConstants } from '@cloudbeaver/core-utils'; @@ -14,44 +14,84 @@ import { type Compartment, type Extension, type Transport, LSPClient, languageSe const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6')); const LSP_ENDPOINT = 'ws/lsp'; +const RECONNECT_BASE_DELAY = 1000; +const MAX_RECONNECT_ATTEMPTS = 5; -function simpleWebSocketTransport(uri: string): Promise { +interface IReconnectingTransport extends Transport { + dispose(): void; +} + +function createReconnectingTransport(uri: string): IReconnectingTransport { let handlers: ((value: string) => void)[] = []; - const sock = new WebSocket(uri); + let sock: WebSocket | null = null; + let reconnectAttempts = 0; + let reconnectTimeout: ReturnType | null = null; + let disposed = false; + + function connect() { + if (disposed) { + return; + } - sock.onmessage = e => { - handlers.forEach(h => h(e.data as string)); - }; + sock = new WebSocket(uri); - sock.onerror = e => { - console.error('[LSP] WebSocket error:', e); - }; + sock.onopen = () => { + reconnectAttempts = 0; + }; - sock.onclose = () => { - console.log('[LSP] WebSocket connection closed'); - }; + sock.onmessage = e => { + handlers.forEach(h => h(e.data as string)); + }; - return new Promise((resolve, reject) => { - sock.onopen = () => { - console.log('[LSP] WebSocket connection established'); - resolve({ - send(message: string) { - sock.send(message); - }, - subscribe(handler) { - handlers.push(handler); - }, - unsubscribe(handler) { - handlers = handlers.filter(h => h !== handler); - }, - }); + sock.onclose = () => { + if (disposed) { + return; + } + + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + const delay = RECONNECT_BASE_DELAY * 2 ** (reconnectAttempts - 1); + console.warn(`[LSP] WebSocket closed, reconnecting in ${delay}ms (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`); + reconnectTimeout = setTimeout(connect, delay); + } else { + console.error('[LSP] Max reconnect attempts reached'); + } }; sock.onerror = e => { - console.error('[LSP] Failed to connect to WebSocket:', e); - reject(e); + console.error('[LSP] WebSocket error:', e); }; - }); + } + + connect(); + + return { + send(message: string) { + if (sock?.readyState === WebSocket.OPEN) { + sock.send(message); + } + }, + subscribe(handler) { + handlers.push(handler); + }, + unsubscribe(handler) { + handlers = handlers.filter(h => h !== handler); + }, + dispose() { + disposed = true; + + if (reconnectTimeout !== null) { + clearTimeout(reconnectTimeout); + } + + if (sock) { + sock.close(); + sock = null; + } + + handlers = []; + }, + }; } export interface ILSPExtensionOptions { @@ -70,24 +110,26 @@ export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Ex return new codemirror.Compartment(); }, [codemirror]); - const client = useMemo(() => { + const { client, transport } = useMemo(() => { const lspClient = new LSPClient({ extensions: languageServerExtensions(), }); const lspServerUrl = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT); + const lspTransport = createReconnectingTransport(lspServerUrl); - simpleWebSocketTransport(lspServerUrl) - .then(transport => { - lspClient.connect(transport); - }) - .catch(error => { - console.error('[LSP] Failed to initialize LSP client:', error); - }); + lspClient.connect(lspTransport); - return lspClient; + return { client: lspClient, transport: lspTransport }; }, []); + useEffect( + () => () => { + transport.dispose(); + }, + [transport], + ); + const documentUri = useMemo(() => { if (!projectId || !resourcePath) { return null; From e44b3bf2f61b5ccae41b05ff678abefd89a72a0e Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Thu, 12 Mar 2026 10:31:19 +0100 Subject: [PATCH 3/8] dbeaver/pro#8065 ci: update codemirror dependencies --- webapp/yarn.lock | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 9f30085dd38..b1e5c8fceeb 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4735,7 +4735,7 @@ __metadata: languageName: unknown linkType: soft -"@codemirror/autocomplete@npm:^6, @codemirror/autocomplete@npm:^6.0.0": +"@codemirror/autocomplete@npm:^6, @codemirror/autocomplete@npm:^6.0.0, @codemirror/autocomplete@npm:^6.20.0": version: 6.20.1 resolution: "@codemirror/autocomplete@npm:6.20.1" dependencies: @@ -4842,7 +4842,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/language@npm:^6, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": +"@codemirror/language@npm:^6, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.11.0, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": version: 6.12.2 resolution: "@codemirror/language@npm:6.12.2" dependencies: @@ -4856,7 +4856,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/lint@npm:^6.0.0": +"@codemirror/lint@npm:^6.0.0, @codemirror/lint@npm:^6.8.5": version: 6.9.5 resolution: "@codemirror/lint@npm:6.9.5" dependencies: @@ -4867,6 +4867,22 @@ __metadata: languageName: node linkType: hard +"@codemirror/lsp-client@npm:^6": + version: 6.2.2 + resolution: "@codemirror/lsp-client@npm:6.2.2" + dependencies: + "@codemirror/autocomplete": "npm:^6.20.0" + "@codemirror/language": "npm:^6.11.0" + "@codemirror/lint": "npm:^6.8.5" + "@codemirror/state": "npm:^6.5.2" + "@codemirror/view": "npm:^6.37.0" + "@lezer/highlight": "npm:^1.2.1" + marked: "npm:^15.0.12" + vscode-languageserver-protocol: "npm:^3.17.5" + checksum: 10c0/909759d953cc189444ce679c6c64859aa1503272510718cb28dea2f7934aa9e8f3985f2f824eac670d1bb211d273753f28b244b4bb0af313f2da058cbbd3cad8 + languageName: node + linkType: hard + "@codemirror/merge@npm:^6": version: 6.12.0 resolution: "@codemirror/merge@npm:6.12.0" @@ -19710,6 +19726,30 @@ __metadata: languageName: node linkType: hard +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + 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: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 10c0/1e1260de79a2cc8de3e46f2e0182cdc94a7eddab487db5a3bd4ee716f67728e685852707d72c059721ce500447be9a46764a04f0611e94e4321ffa088eef36f8 + languageName: node + linkType: hard + "w3c-keyname@npm:^2.2.4": version: 2.2.8 resolution: "w3c-keyname@npm:2.2.8" From 2207546541a3e37e791274f632f43b03530b3a43 Mon Sep 17 00:00:00 2001 From: Dmitrii Barnukov Date: Thu, 19 Mar 2026 14:54:20 +0100 Subject: [PATCH 4/8] dbeaver/pro#5562 update launcher --- ...vider.java => LSPWebServerSessionProvider.java} | 4 ++-- .../websockets/lsp/LSPWebSocketEndpoint.java | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/{LSPWebServerSesssionProvider.java => LSPWebServerSessionProvider.java} (89%) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSesssionProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSessionProvider.java similarity index 89% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSesssionProvider.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSessionProvider.java index c456010c4bd..00562a74364 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSesssionProvider.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebServerSessionProvider.java @@ -23,12 +23,12 @@ import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; import org.jkiss.dbeaver.model.lsp.DBLServerSessionProvider; -public class LSPWebServerSesssionProvider implements DBLServerSessionProvider { +public class LSPWebServerSessionProvider implements DBLServerSessionProvider { @NotNull private final BaseWebSession session; - public LSPWebServerSesssionProvider(@NotNull BaseWebSession session) { + public LSPWebServerSessionProvider(@NotNull BaseWebSession session) { this.session = session; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java index e7493567ff7..daae2ccf434 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java @@ -44,14 +44,14 @@ public void onOpen(Session wsSession, EndpointConfig endpointConfig) { wsSession.setMaxTextMessageBufferSize(Integer.MAX_VALUE); wsSession.setMaxBinaryMessageBufferSize(Integer.MAX_VALUE); - LSPWebServerSesssionProvider sessionProvider = new LSPWebServerSesssionProvider(webSession); + LSPWebServerSessionProvider sessionProvider = new LSPWebServerSessionProvider(webSession); DBLServer server = new DBLServer(sessionProvider); - var builder = new WebSocketLauncherBuilder(); - builder.setSession(wsSession); - builder.setLocalService(server); - builder.setRemoteInterface(LanguageClient.class); - Launcher launcher = builder.create(); - server.connect(launcher.getRemoteProxy()); + Launcher launcher = new WebSocketLauncherBuilder() + .setSession(wsSession) + .setLocalService(server) + .setRemoteInterface(LanguageClient.class) + .create(); + launcher.startListening(); } @Override From 2d2dbeb79a6259393760292067889aa4c0361828 Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Thu, 19 Mar 2026 15:21:36 +0100 Subject: [PATCH 5/8] dbeaver/pro#8065 feat: add LSPConnectionService and module integration --- .../packages/plugin-set-common/package.json | 1 + .../packages/plugin-set-common/src/index.ts | 2 + .../packages/plugin-set-common/tsconfig.json | 3 + .../plugin-sql-editor-codemirror/package.json | 1 + .../src/LSPConnectionService.ts | 125 ++++++++++++++++++ .../src/module.ts | 18 +++ .../src/useLSPExtension.ts | 108 +-------------- .../tsconfig.json | 3 + webapp/yarn.lock | 2 + 9 files changed, 162 insertions(+), 101 deletions(-) create mode 100644 webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts create mode 100644 webapp/packages/plugin-sql-editor-codemirror/src/module.ts diff --git a/webapp/packages/plugin-set-common/package.json b/webapp/packages/plugin-set-common/package.json index df2262d75e0..2f27c1ea5ca 100644 --- a/webapp/packages/plugin-set-common/package.json +++ b/webapp/packages/plugin-set-common/package.json @@ -110,6 +110,7 @@ "@cloudbeaver/plugin-settings-panel": "workspace:*", "@cloudbeaver/plugin-sql-async-task-confirmation": "workspace:^", "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-sql-editor-codemirror": "workspace:*", "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*", "@cloudbeaver/plugin-sql-editor-navigation-tab-script": "workspace:*", "@cloudbeaver/plugin-sql-editor-new": "workspace:*", diff --git a/webapp/packages/plugin-set-common/src/index.ts b/webapp/packages/plugin-set-common/src/index.ts index b493f11bb3a..881c73a5c76 100644 --- a/webapp/packages/plugin-set-common/src/index.ts +++ b/webapp/packages/plugin-set-common/src/index.ts @@ -90,6 +90,7 @@ import pluginSettingsPanel from '@cloudbeaver/plugin-settings-panel/module'; import pluginSqlEditor from '@cloudbeaver/plugin-sql-editor/module'; import pluginSqlEditorNavigationTab from '@cloudbeaver/plugin-sql-editor-navigation-tab/module'; import pluginSqlEditorNavigationTabScript from '@cloudbeaver/plugin-sql-editor-navigation-tab-script/module'; +import pluginSqlEditorCodemirror from '@cloudbeaver/plugin-sql-editor-codemirror/module'; import pluginSqlEditorNew from '@cloudbeaver/plugin-sql-editor-new/module'; import pluginSqlEditorScreen from '@cloudbeaver/plugin-sql-editor-screen/module'; import pluginSqlGenerator from '@cloudbeaver/plugin-sql-generator/module'; @@ -175,6 +176,7 @@ export const commonSet = [ pluginSqlEditorNavigationTab, pluginSqlEditorScreen, pluginSqlEditorNew, + pluginSqlEditorCodemirror, pluginSqlGenerator, pluginUserProfile, pluginUserProfileAdministration, diff --git a/webapp/packages/plugin-set-common/tsconfig.json b/webapp/packages/plugin-set-common/tsconfig.json index 2c341834f0f..eaa3ae4b816 100644 --- a/webapp/packages/plugin-set-common/tsconfig.json +++ b/webapp/packages/plugin-set-common/tsconfig.json @@ -274,6 +274,9 @@ { "path": "../plugin-sql-editor" }, + { + "path": "../plugin-sql-editor-codemirror" + }, { "path": "../plugin-sql-editor-navigation-tab" }, diff --git a/webapp/packages/plugin-sql-editor-codemirror/package.json b/webapp/packages/plugin-sql-editor-codemirror/package.json index 8eacfb41229..f299d9329e3 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/package.json +++ b/webapp/packages/plugin-sql-editor-codemirror/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", "@cloudbeaver/core-sdk": "workspace:*", "@cloudbeaver/core-utils": "workspace:*", "@cloudbeaver/plugin-codemirror6": "workspace:*", diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts new file mode 100644 index 00000000000..44c903bac4a --- /dev/null +++ b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts @@ -0,0 +1,125 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; +import { type Transport, LSPClient, languageServerExtensions } from '@cloudbeaver/plugin-codemirror6'; + +const LSP_ENDPOINT = 'ws/lsp'; +const RECONNECT_BASE_DELAY = 1000; +const MAX_RECONNECT_ATTEMPTS = 5; + +interface IReconnectingTransport extends Transport { + dispose(): void; +} + +function createReconnectingTransport(uri: string): IReconnectingTransport { + let handlers: ((value: string) => void)[] = []; + let sock: WebSocket | null = null; + let reconnectAttempts = 0; + let reconnectTimeout: ReturnType | null = null; + let disposed = false; + + function connect() { + if (disposed) { + return; + } + + sock = new WebSocket(uri); + + sock.onopen = () => { + console.log('[LSP] WebSocket connected'); + reconnectAttempts = 0; + }; + + sock.onmessage = e => { + handlers.forEach(h => h(e.data as string)); + }; + + sock.onclose = () => { + if (disposed) { + return; + } + + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + const delay = RECONNECT_BASE_DELAY * 2 ** (reconnectAttempts - 1); + console.warn(`[LSP] WebSocket closed, reconnecting in ${delay}ms (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`); + reconnectTimeout = setTimeout(connect, delay); + } else { + console.error('[LSP] Max reconnect attempts reached'); + } + }; + + sock.onerror = e => { + console.error('[LSP] WebSocket error:', e); + }; + } + + connect(); + + return { + send(message: string) { + if (sock?.readyState === WebSocket.OPEN) { + sock.send(message); + } + }, + subscribe(handler) { + handlers.push(handler); + }, + unsubscribe(handler) { + handlers = handlers.filter(h => h !== handler); + }, + dispose() { + disposed = true; + + if (reconnectTimeout !== null) { + clearTimeout(reconnectTimeout); + } + + if (sock) { + sock.close(); + sock = null; + } + + handlers = []; + }, + }; +} + +@injectable() +export class LSPConnectionService { + private client: LSPClient | null = null; + private transport: IReconnectingTransport | null = null; + private refCount = 0; + + acquire(): LSPClient { + if (!this.client) { + this.client = new LSPClient({ + extensions: languageServerExtensions(), + }); + + const url = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT); + this.transport = createReconnectingTransport(url); + this.client.connect(this.transport); + } + + this.refCount++; + return this.client; + } + + release(): void { + this.refCount--; + + if (this.refCount <= 0) { + this.transport?.dispose(); + this.client = null; + this.transport = null; + this.refCount = 0; + } + } +} diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/module.ts b/webapp/packages/plugin-sql-editor-codemirror/src/module.ts new file mode 100644 index 00000000000..dcaab497912 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-codemirror/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ModuleRegistry } from '@cloudbeaver/core-di'; + +import { LSPConnectionService } from './LSPConnectionService.js'; + +export default ModuleRegistry.add({ + name: '@cloudbeaver/plugin-sql-editor-codemirror', + + configure: serviceCollection => { + serviceCollection.addSingleton(LSPConnectionService); + }, +}); diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts index c7be6f7d055..9fc5e30cea8 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts @@ -8,91 +8,12 @@ import { useEffect, useMemo } from 'react'; import { createLazyLoader, useLazyImport } from '@cloudbeaver/core-blocks'; -import { GlobalConstants } from '@cloudbeaver/core-utils'; -import { type Compartment, type Extension, type Transport, LSPClient, languageServerExtensions } from '@cloudbeaver/plugin-codemirror6'; +import { useService } from '@cloudbeaver/core-di'; +import { type Compartment, type Extension } from '@cloudbeaver/plugin-codemirror6'; -const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6')); - -const LSP_ENDPOINT = 'ws/lsp'; -const RECONNECT_BASE_DELAY = 1000; -const MAX_RECONNECT_ATTEMPTS = 5; - -interface IReconnectingTransport extends Transport { - dispose(): void; -} - -function createReconnectingTransport(uri: string): IReconnectingTransport { - let handlers: ((value: string) => void)[] = []; - let sock: WebSocket | null = null; - let reconnectAttempts = 0; - let reconnectTimeout: ReturnType | null = null; - let disposed = false; - - function connect() { - if (disposed) { - return; - } - - sock = new WebSocket(uri); - - sock.onopen = () => { - reconnectAttempts = 0; - }; - - sock.onmessage = e => { - handlers.forEach(h => h(e.data as string)); - }; - - sock.onclose = () => { - if (disposed) { - return; - } +import { LSPConnectionService } from './LSPConnectionService.js'; - if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { - reconnectAttempts++; - const delay = RECONNECT_BASE_DELAY * 2 ** (reconnectAttempts - 1); - console.warn(`[LSP] WebSocket closed, reconnecting in ${delay}ms (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`); - reconnectTimeout = setTimeout(connect, delay); - } else { - console.error('[LSP] Max reconnect attempts reached'); - } - }; - - sock.onerror = e => { - console.error('[LSP] WebSocket error:', e); - }; - } - - connect(); - - return { - send(message: string) { - if (sock?.readyState === WebSocket.OPEN) { - sock.send(message); - } - }, - subscribe(handler) { - handlers.push(handler); - }, - unsubscribe(handler) { - handlers = handlers.filter(h => h !== handler); - }, - dispose() { - disposed = true; - - if (reconnectTimeout !== null) { - clearTimeout(reconnectTimeout); - } - - if (sock) { - sock.close(); - sock = null; - } - - handlers = []; - }, - }; -} +const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6')); export interface ILSPExtensionOptions { projectId: string | null | undefined; @@ -102,6 +23,7 @@ export interface ILSPExtensionOptions { export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Extension] | null { const { projectId, resourcePath } = options; const codemirror = useLazyImport(codemirrorPluginLoader); + const lspConnectionService = useService(LSPConnectionService); const LSP_COMPARTMENT = useMemo(() => { if (!codemirror) { @@ -110,25 +32,9 @@ export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Ex return new codemirror.Compartment(); }, [codemirror]); - const { client, transport } = useMemo(() => { - const lspClient = new LSPClient({ - extensions: languageServerExtensions(), - }); - - const lspServerUrl = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT); - const lspTransport = createReconnectingTransport(lspServerUrl); - - lspClient.connect(lspTransport); - - return { client: lspClient, transport: lspTransport }; - }, []); + const client = useMemo(() => lspConnectionService.acquire(), [lspConnectionService]); - useEffect( - () => () => { - transport.dispose(); - }, - [transport], - ); + useEffect(() => () => lspConnectionService.release(), [lspConnectionService]); const documentUri = useMemo(() => { if (!projectId || !resourcePath) { diff --git a/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json b/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json index abff46a96ea..b531e55bdc5 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-codemirror/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../core-cli" }, + { + "path": "../core-di" + }, { "path": "../core-sdk" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 32a7434b5c7..d6a694eb37c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -3954,6 +3954,7 @@ __metadata: "@cloudbeaver/plugin-settings-panel": "workspace:*" "@cloudbeaver/plugin-sql-async-task-confirmation": "workspace:^" "@cloudbeaver/plugin-sql-editor": "workspace:*" + "@cloudbeaver/plugin-sql-editor-codemirror": "workspace:*" "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*" "@cloudbeaver/plugin-sql-editor-navigation-tab-script": "workspace:*" "@cloudbeaver/plugin-sql-editor-new": "workspace:*" @@ -4116,6 +4117,7 @@ __metadata: dependencies: "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-di": "workspace:*" "@cloudbeaver/core-sdk": "workspace:*" "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/plugin-codemirror6": "workspace:*" From 1998bd09c18f43bfc8e8bba3814033a70fb1c65b Mon Sep 17 00:00:00 2001 From: Dmitrii Barnukov Date: Thu, 19 Mar 2026 17:21:18 +0100 Subject: [PATCH 6/8] dbeaver/pro#5562 update launcher --- .../cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java index daae2ccf434..7492e210176 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/lsp/LSPWebSocketEndpoint.java @@ -51,7 +51,7 @@ public void onOpen(Session wsSession, EndpointConfig endpointConfig) { .setLocalService(server) .setRemoteInterface(LanguageClient.class) .create(); - launcher.startListening(); + server.connect(launcher.getRemoteProxy()); } @Override From 3f1ae18ea5cc3d8651c3ce1b2e5a49e38e953558 Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Fri, 20 Mar 2026 11:44:17 +0100 Subject: [PATCH 7/8] dbeaver/pro#8065 feat: enhance LSPConnectionService with reconnecting transport and promise-based readiness --- .../src/LSPConnectionService.ts | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts index 44c903bac4a..aaa8b7a8acb 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts @@ -17,12 +17,47 @@ interface IReconnectingTransport extends Transport { dispose(): void; } -function createReconnectingTransport(uri: string): IReconnectingTransport { +function createReconnectingTransport(uri: string): { ready: Promise; dispose(): void } { let handlers: ((value: string) => void)[] = []; let sock: WebSocket | null = null; let reconnectAttempts = 0; let reconnectTimeout: ReturnType | null = null; let disposed = false; + let connected = false; + + const { promise: ready, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers(); + + const transport: IReconnectingTransport = { + send(message: string) { + if (sock?.readyState === WebSocket.OPEN) { + sock.send(message); + } + }, + subscribe(handler) { + handlers.push(handler); + }, + unsubscribe(handler) { + handlers = handlers.filter(h => h !== handler); + }, + dispose() { + disposed = true; + + if (reconnectTimeout !== null) { + clearTimeout(reconnectTimeout); + } + + if (sock) { + sock.onopen = null; + sock.onmessage = null; + sock.onclose = null; + sock.onerror = null; + sock.close(); + sock = null; + } + + handlers = []; + }, + }; function connect() { if (disposed) { @@ -32,8 +67,12 @@ function createReconnectingTransport(uri: string): IReconnectingTransport { sock = new WebSocket(uri); sock.onopen = () => { - console.log('[LSP] WebSocket connected'); reconnectAttempts = 0; + + if (!connected) { + connected = true; + resolveReady(transport); + } }; sock.onmessage = e => { @@ -52,6 +91,7 @@ function createReconnectingTransport(uri: string): IReconnectingTransport { reconnectTimeout = setTimeout(connect, delay); } else { console.error('[LSP] Max reconnect attempts reached'); + rejectReady(new Error('LSP WebSocket connection failed after max reconnect attempts')); } }; @@ -62,39 +102,13 @@ function createReconnectingTransport(uri: string): IReconnectingTransport { connect(); - return { - send(message: string) { - if (sock?.readyState === WebSocket.OPEN) { - sock.send(message); - } - }, - subscribe(handler) { - handlers.push(handler); - }, - unsubscribe(handler) { - handlers = handlers.filter(h => h !== handler); - }, - dispose() { - disposed = true; - - if (reconnectTimeout !== null) { - clearTimeout(reconnectTimeout); - } - - if (sock) { - sock.close(); - sock = null; - } - - handlers = []; - }, - }; + return { ready, dispose: transport.dispose }; } @injectable() export class LSPConnectionService { private client: LSPClient | null = null; - private transport: IReconnectingTransport | null = null; + private disposeTransport: (() => void) | null = null; private refCount = 0; acquire(): LSPClient { @@ -104,8 +118,18 @@ export class LSPConnectionService { }); const url = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT); - this.transport = createReconnectingTransport(url); - this.client.connect(this.transport); + const { ready, dispose } = createReconnectingTransport(url); + this.disposeTransport = dispose; + + ready + .then(transport => { + if (this.client) { + this.client.connect(transport); + } + }) + .catch(error => { + console.error(error); + }); } this.refCount++; @@ -116,10 +140,10 @@ export class LSPConnectionService { this.refCount--; if (this.refCount <= 0) { - this.transport?.dispose(); this.client = null; - this.transport = null; this.refCount = 0; + this.disposeTransport?.(); + this.disposeTransport = null; } } } From bd98225b3c6638cbfed161c4a3456c994efec4eb Mon Sep 17 00:00:00 2001 From: Sychev Andrey Date: Mon, 23 Mar 2026 10:45:34 +0100 Subject: [PATCH 8/8] dbeaver/pro#8065 feat: integrate LSPConnectionService for document formatting and update SQL editor to use LSP document URI --- .../src/ConnectionDialectResource.ts | 12 ------- .../src/queries/sql-editor/formatSqlQuery.gql | 13 ------- .../src/LSPConnectionService.ts | 34 +++++++++++++++++++ .../plugin-sql-editor-codemirror/src/index.ts | 1 + .../src/useLSPExtension.ts | 12 ++----- .../src/ResourceSqlDataSource.ts | 10 +++++- .../SQLCodeEditorPanel/SQLCodeEditorPanel.tsx | 9 ++--- .../src/SqlDataSource/BaseSqlDataSource.ts | 4 +++ .../src/SqlDataSource/ISqlDataSource.ts | 3 +- .../src/SqlDialectInfoService.ts | 27 --------------- .../src/SqlEditor/useSqlEditor.ts | 30 +++++++++------- .../packages/plugin-sql-editor/src/index.ts | 1 - .../packages/plugin-sql-editor/src/module.ts | 2 -- 13 files changed, 74 insertions(+), 84 deletions(-) delete mode 100644 webapp/packages/core-sdk/src/queries/sql-editor/formatSqlQuery.gql delete mode 100644 webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts diff --git a/webapp/packages/core-connections/src/ConnectionDialectResource.ts b/webapp/packages/core-connections/src/ConnectionDialectResource.ts index f58e5aa407a..d07071fbf08 100644 --- a/webapp/packages/core-connections/src/ConnectionDialectResource.ts +++ b/webapp/packages/core-connections/src/ConnectionDialectResource.ts @@ -12,7 +12,6 @@ import { CachedMapAllKey, CachedMapResource, type ResourceKey, resourceKeyList, import { GraphQLService, type SqlDialectInfo } from '@cloudbeaver/core-sdk'; import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js'; -import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContext/ConnectionExecutionContextResource.js'; import { ConnectionInfoActiveProjectKey, ConnectionInfoProjectKey, @@ -42,17 +41,6 @@ export class ConnectionDialectResource extends CachedMapResource !connectionInfoResource.isConnected(key))); } - async formatScript(context: IConnectionExecutionContextInfo, query: string): Promise { - const result = await this.graphQLService.sdk.formatSqlQuery({ - projectId: context.projectId, - connectionId: context.connectionId, - contextId: context.id, - query, - }); - - return result.query; - } - protected async loader( originalKey: ResourceKey, includes: string[], diff --git a/webapp/packages/core-sdk/src/queries/sql-editor/formatSqlQuery.gql b/webapp/packages/core-sdk/src/queries/sql-editor/formatSqlQuery.gql deleted file mode 100644 index ae87badc4fc..00000000000 --- a/webapp/packages/core-sdk/src/queries/sql-editor/formatSqlQuery.gql +++ /dev/null @@ -1,13 +0,0 @@ -query formatSqlQuery( - $projectId: ID! - $connectionId: ID! - $contextId: ID! - $query: String! -) { - query: sqlFormatQuery( - projectId: $projectId - connectionId: $connectionId - contextId: $contextId - query: $query - ) -} diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts index aaa8b7a8acb..4130b04237a 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/LSPConnectionService.ts @@ -17,6 +17,21 @@ interface IReconnectingTransport extends Transport { dispose(): void; } +interface ILSPPosition { + line: number; + character: number; +} + +interface ILSPTextEdit { + range: { start: ILSPPosition; end: ILSPPosition }; + newText: string; +} + +interface IDocumentFormattingParams { + textDocument: { uri: string }; + options: { tabSize: number; insertSpaces: boolean }; +} + function createReconnectingTransport(uri: string): { ready: Promise; dispose(): void } { let handlers: ((value: string) => void)[] = []; let sock: WebSocket | null = null; @@ -136,6 +151,25 @@ export class LSPConnectionService { return this.client; } + async formatDocument(documentUri: string): Promise { + if (!this.client) { + return null; + } + + this.client.sync(); + + const result = await this.client.request('textDocument/formatting', { + textDocument: { uri: documentUri }, + options: { tabSize: 4, insertSpaces: true }, + }); + + if (!result?.length) { + return null; + } + + return result[0]!.newText; + } + release(): void { this.refCount--; diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/index.ts b/webapp/packages/plugin-sql-editor-codemirror/src/index.ts index 594027ce277..924c20d4994 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/index.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/index.ts @@ -10,5 +10,6 @@ export * from './SQLCodeEditor/SQLCodeEditorLoader.js'; export * from './SQLCodeEditor/useSQLCodeEditor.js'; export * from './useSqlDialectExtension.js'; export * from './useLSPExtension.js'; +export * from './LSPConnectionService.js'; export * from './ACTIVE_QUERY_EXTENSION.js'; export * from './QUERY_STATUS_GUTTER_EXTENSION.js'; diff --git a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts index 9fc5e30cea8..607b4d3f23d 100644 --- a/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts +++ b/webapp/packages/plugin-sql-editor-codemirror/src/useLSPExtension.ts @@ -16,12 +16,11 @@ import { LSPConnectionService } from './LSPConnectionService.js'; const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6')); export interface ILSPExtensionOptions { - projectId: string | null | undefined; - resourcePath: string | null | undefined; + documentUri: string | null | undefined; } export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Extension] | null { - const { projectId, resourcePath } = options; + const { documentUri } = options; const codemirror = useLazyImport(codemirrorPluginLoader); const lspConnectionService = useService(LSPConnectionService); @@ -36,13 +35,6 @@ export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Ex useEffect(() => () => lspConnectionService.release(), [lspConnectionService]); - const documentUri = useMemo(() => { - if (!projectId || !resourcePath) { - return null; - } - return `lsp://${projectId}/${resourcePath}`; - }, [projectId, resourcePath]); - return useMemo(() => { if (!LSP_COMPARTMENT || !client || !codemirror || !documentUri) { return null; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts index b9974a8dd75..3279f2ce45a 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -73,6 +73,14 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return key.projectId; } + override get lspDocumentUri(): string | null { + if (!this.resourceKey || !this.projectId) { + return null; + } + const { path } = getRmResourceKey(this.resourceKey); + return `lsp://${this.projectId}/${path}`; + } + get executionContext(): IConnectionExecutionContextInfo | undefined { const executionContext = this.state?.executionContext; if (!executionContext || !this.connectionInfoResource.has(createConnectionParam(executionContext.projectId, executionContext.connectionId))) { diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx index e16fa3b113b..be03fbad1d8 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx @@ -47,10 +47,11 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent const sqlDialect = useSqlDialectExtension(data.dialect); const highlightExtensions = useHighlightExtensions(sqlEditorSettingsService.highlightWhitespace); - const lspExtension = useLSPExtension({ - projectId: data.model.dataSource?.projectId, - resourcePath: data.model.state?.editorId, - }); + const lspDocumentUri = + data.model.dataSource?.lspDocumentUri ?? + (data.model.dataSource?.projectId ? `lsp://${data.model.dataSource.projectId}/${data.model.state?.editorId}` : null); + + const lspExtension = useLSPExtension({ documentUri: lspDocumentUri }); if (autocompletion) { extensions.set(...autocompletion); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 2374de03178..b5c439dbc64 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -77,6 +77,10 @@ export abstract class BaseSqlDataSource [ConnectionDialectResource, NotificationService]) -export class SqlDialectInfoService { - constructor( - private readonly connectionDialectResource: ConnectionDialectResource, - private readonly notificationService: NotificationService, - ) {} - - async formatScript(context: IConnectionExecutionContextInfo, query: string): Promise { - try { - return await this.connectionDialectResource.formatScript(context, query); - } catch (error: any) { - this.notificationService.logException(error, 'Failed to format script'); - } - return query; - } -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 3b6a886c2ad..c0b5a01b5a7 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -19,7 +19,7 @@ import { createLastPromiseGetter, type LastPromiseGetter } from '@cloudbeaver/co import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js'; import type { ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; -import { SqlDialectInfoService } from '../SqlDialectInfoService.js'; +import { LSPConnectionService } from '@cloudbeaver/plugin-sql-editor-codemirror'; import { SqlEditorService } from '../SqlEditorService.js'; import { type ISQLScriptSegment } from '../SQLParser.js'; import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.js'; @@ -31,7 +31,7 @@ import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js'; import { SqlEditorModelService } from '../SqlEditorModel/SqlEditorModelService.js'; interface ISQLEditorDataPrivate extends ISQLEditorData { - readonly sqlDialectInfoService: SqlDialectInfoService; + readonly lspConnectionService: LSPConnectionService; readonly connectionExecutionContextService: ConnectionExecutionContextService; readonly sqlQueryService: SqlQueryService; readonly sqlEditorService: SqlEditorService; @@ -56,7 +56,7 @@ const MAX_HINTS_LIMIT = 200; export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const connectionExecutionContextService = useService(ConnectionExecutionContextService); const sqlQueryService = useService(SqlQueryService); - const sqlDialectInfoService = useService(SqlDialectInfoService); + const lspConnectionService = useService(LSPConnectionService); const sqlEditorService = useService(SqlEditorService); const notificationService = useService(NotificationService); const sqlExecutionPlanService = useService(SqlExecutionPlanService); @@ -155,26 +155,30 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async formatScript(): Promise { - if (this.isDisabled || this.isScriptEmpty || !this.model.dataSource?.executionContext) { + if (this.isDisabled || this.isScriptEmpty) { return; } - const script = await this.model.getResolvedSegment(); + const documentUri = + this.model.dataSource?.lspDocumentUri ?? + (this.model.dataSource?.projectId ? `lsp://${this.model.dataSource.projectId}/${this.state.editorId}` : null); - if (!script) { + if (!documentUri) { return; } this.onExecute.execute(true); try { this.readonlyState = true; - const formatted = await this.sqlDialectInfoService.formatScript(this.model.dataSource.executionContext, script.query); + const formatted = await this.lspConnectionService.formatDocument(documentUri); - const cursorAnchor = this.model.cursor.anchor; - this.setScript(this.value.substring(0, script.begin) + formatted + this.value.substring(script.end), 'format', { - anchor: cursorAnchor, - head: cursorAnchor, - }); + if (formatted !== null) { + const cursorAnchor = this.model.cursor.anchor; + this.setScript(formatted, 'format', { + anchor: cursorAnchor, + head: cursorAnchor, + }); + } } finally { this.readonlyState = false; } @@ -352,9 +356,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { state, model, dialect: connectionDialectLoader.tryGetData, + lspConnectionService, connectionExecutionContextService, sqlQueryService, - sqlDialectInfoService, sqlEditorService, sqlExecutionPlanService, sqlResultTabsService, diff --git a/webapp/packages/plugin-sql-editor/src/index.ts b/webapp/packages/plugin-sql-editor/src/index.ts index f6a6a39475b..1c2cf91520a 100644 --- a/webapp/packages/plugin-sql-editor/src/index.ts +++ b/webapp/packages/plugin-sql-editor/src/index.ts @@ -46,7 +46,6 @@ export * from './SqlEditorModel/SqlEditorModelService.js'; export * from './DATA_CONTEXT_SQL_EDITOR_STATE.js'; export * from './getSqlEditorName.js'; export * from './QueryDataSource.js'; -export * from './SqlDialectInfoService.js'; export * from './ISqlEditorTabState.js'; export * from './SQLEditorLoader.js'; export * from './SqlEditorModeService.js'; diff --git a/webapp/packages/plugin-sql-editor/src/module.ts b/webapp/packages/plugin-sql-editor/src/module.ts index 7b4d16819d2..5e19e25a6fa 100644 --- a/webapp/packages/plugin-sql-editor/src/module.ts +++ b/webapp/packages/plugin-sql-editor/src/module.ts @@ -20,7 +20,6 @@ import { SqlEditorSettingsService } from './SqlEditorSettingsService.js'; import { SqlEditorService } from './SqlEditorService.js'; import { SqlEditorModeService } from './SqlEditorModeService.js'; import { SqlEditorGroupTabsBootstrap } from './SqlEditorGroupTabsBootstrap.js'; -import { SqlDialectInfoService } from './SqlDialectInfoService.js'; import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; import { LocalStorageSqlDataSourceBootstrap } from './SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.js'; import { MenuBootstrap } from './MenuBootstrap.js'; @@ -54,7 +53,6 @@ export default ModuleRegistry.add({ .addSingleton(SqlEditorService) .addSingleton(SqlEditorModeService) .addSingleton(SqlEditorGroupTabsBootstrap) - .addSingleton(SqlDialectInfoService) .addSingleton(SqlDataSourceService) .addSingleton(LocalStorageSqlDataSourceBootstrap) .addSingleton(SqlEditorModelService)