Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
666982b
dbeaver/pro#8065 feat: add LSP client support and integrate into SQL …
SychevAndrey Feb 12, 2026
dc0d245
Merge branch 'devel' into 8065-lsp-front-end
SychevAndrey Feb 12, 2026
5db1593
dbeaver/pro#8065 feat: implement reconnecting WebSocket transport for…
SychevAndrey Feb 12, 2026
7b60aab
Merge branch 'devel' into 8065-lsp-front-end
SychevAndrey Feb 18, 2026
9c6f6c5
Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end
SychevAndrey Feb 18, 2026
144904d
Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end
SychevAndrey Feb 19, 2026
4e2e7e0
Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end
SychevAndrey Feb 19, 2026
e3c653c
Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end
Nexus6v2 Mar 11, 2026
567cd9a
Merge remote-tracking branch 'origin/dbeaver/pro#7844-cb-lsp-server' …
SychevAndrey Mar 12, 2026
e44b3bf
dbeaver/pro#8065 ci: update codemirror dependencies
SychevAndrey Mar 12, 2026
e08d768
Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end
SychevAndrey Mar 19, 2026
2207546
dbeaver/pro#5562 update launcher
Nexus6v2 Mar 19, 2026
2d2dbeb
dbeaver/pro#8065 feat: add LSPConnectionService and module integration
SychevAndrey Mar 19, 2026
1998bd0
dbeaver/pro#5562 update launcher
Nexus6v2 Mar 19, 2026
f184e6f
Merge remote-tracking branch 'origin/8065-lsp-front-end' into 8065-ls…
Nexus6v2 Mar 19, 2026
3f1ae18
dbeaver/pro#8065 feat: enhance LSPConnectionService with reconnecting…
SychevAndrey Mar 20, 2026
bd98225
dbeaver/pro#8065 feat: integrate LSPConnectionService for document fo…
SychevAndrey Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ 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<LanguageClient>();
builder.setSession(wsSession);
builder.setLocalService(server);
builder.setRemoteInterface(LanguageClient.class);
Launcher<LanguageClient> launcher = builder.create();
Launcher<LanguageClient> launcher = new WebSocketLauncherBuilder<LanguageClient>()
.setSession(wsSession)
.setLocalService(server)
.setRemoteInterface(LanguageClient.class)
.create();
server.connect(launcher.getRemoteProxy());
}

Expand Down
12 changes: 0 additions & 12 deletions webapp/packages/core-connections/src/ConnectionDialectResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,17 +41,6 @@ export class ConnectionDialectResource extends CachedMapResource<IConnectionInfo
this.before(ExecutorInterrupter.interrupter(key => !connectionInfoResource.isConnected(key)));
}

async formatScript(context: IConnectionExecutionContextInfo, query: string): Promise<string> {
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<IConnectionInfoParams>,
includes: string[],
Expand Down
13 changes: 0 additions & 13 deletions webapp/packages/core-sdk/src/queries/sql-editor/formatSqlQuery.gql

This file was deleted.

1 change: 1 addition & 0 deletions webapp/packages/plugin-codemirror6/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion webapp/packages/plugin-codemirror6/src/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -9,7 +9,7 @@
import './module.js';
import { createLazyLoader } from '@cloudbeaver/core-blocks';

export * from './ReactCodemirrorPanel.js';

Check failure on line 12 in webapp/packages/plugin-codemirror6/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './EditorLoader.js';
export * from './IEditorProps.js';
export * from './IEditorRef.js';
Expand All @@ -24,6 +24,7 @@
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';
Expand Down
1 change: 1 addition & 0 deletions webapp/packages/plugin-set-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-set-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -175,6 +176,7 @@ export const commonSet = [
pluginSqlEditorNavigationTab,
pluginSqlEditorScreen,
pluginSqlEditorNew,
pluginSqlEditorCodemirror,
pluginSqlGenerator,
pluginUserProfile,
pluginUserProfileAdministration,
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/plugin-set-common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@
{
"path": "../plugin-sql-editor"
},
{
"path": "../plugin-sql-editor-codemirror"
},
{
"path": "../plugin-sql-editor-navigation-tab"
},
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-sql-editor-codemirror/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
},
"dependencies": {
"@cloudbeaver/core-blocks": "workspace:*",
"@cloudbeaver/core-di": "workspace:*",
"@cloudbeaver/core-sdk": "workspace:*",
"@cloudbeaver/core-utils": "workspace:*",
"@cloudbeaver/plugin-codemirror6": "workspace:*",
"mobx": "^6",
"mobx-react-lite": "^4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* 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;
}

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<IReconnectingTransport>; dispose(): void } {
let handlers: ((value: string) => void)[] = [];
let sock: WebSocket | null = null;
let reconnectAttempts = 0;
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
let disposed = false;
let connected = false;

const { promise: ready, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<IReconnectingTransport>();

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) {
return;
}

sock = new WebSocket(uri);

sock.onopen = () => {
reconnectAttempts = 0;

if (!connected) {
connected = true;
resolveReady(transport);
}
};

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');
rejectReady(new Error('LSP WebSocket connection failed after max reconnect attempts'));
}
};

sock.onerror = e => {
console.error('[LSP] WebSocket error:', e);
};
}

connect();

return { ready, dispose: transport.dispose };
}

@injectable()
export class LSPConnectionService {
private client: LSPClient | null = null;
private disposeTransport: (() => void) | null = null;
private refCount = 0;

acquire(): LSPClient {
if (!this.client) {
this.client = new LSPClient({
extensions: languageServerExtensions(),
});

const url = GlobalConstants.absoluteServiceWSUrl(LSP_ENDPOINT);
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++;
return this.client;
}

async formatDocument(documentUri: string): Promise<string | null> {
if (!this.client) {
return null;
}

this.client.sync();

const result = await this.client.request<IDocumentFormattingParams, ILSPTextEdit[] | null>('textDocument/formatting', {
textDocument: { uri: documentUri },
options: { tabSize: 4, insertSpaces: true },
});

if (!result?.length) {
return null;
}

return result[0]!.newText;
}

release(): void {
this.refCount--;

if (this.refCount <= 0) {
this.client = null;
this.refCount = 0;
this.disposeTransport?.();
this.disposeTransport = null;
}
}
}
2 changes: 2 additions & 0 deletions webapp/packages/plugin-sql-editor-codemirror/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
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';
18 changes: 18 additions & 0 deletions webapp/packages/plugin-sql-editor-codemirror/src/module.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { useEffect, useMemo } from 'react';

import { createLazyLoader, useLazyImport } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { type Compartment, type Extension } from '@cloudbeaver/plugin-codemirror6';

import { LSPConnectionService } from './LSPConnectionService.js';

const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6'));

export interface ILSPExtensionOptions {
documentUri: string | null | undefined;
}

export function useLSPExtension(options: ILSPExtensionOptions): [Compartment, Extension] | null {
const { documentUri } = options;
const codemirror = useLazyImport(codemirrorPluginLoader);
const lspConnectionService = useService(LSPConnectionService);

const LSP_COMPARTMENT = useMemo(() => {
if (!codemirror) {
return null;
}
return new codemirror.Compartment();
}, [codemirror]);

const client = useMemo(() => lspConnectionService.acquire(), [lspConnectionService]);

useEffect(() => () => lspConnectionService.release(), [lspConnectionService]);

return useMemo(() => {
if (!LSP_COMPARTMENT || !client || !codemirror || !documentUri) {
return null;
}

return [LSP_COMPARTMENT, client.plugin(documentUri)];
}, [LSP_COMPARTMENT, client, codemirror, documentUri]);
}
Loading
Loading