Compare commits

...
Author SHA1 Message Date
Sychev Andrey bd98225b3c dbeaver/pro#8065 feat: integrate LSPConnectionService for document formatting and update SQL editor to use LSP document URI 2026-03-23 10:45:34 +01:00
Sychev Andrey 3f1ae18ea5 dbeaver/pro#8065 feat: enhance LSPConnectionService with reconnecting transport and promise-based readiness 2026-03-20 11:44:17 +01:00
Dmitrii Barnukov f184e6f28c Merge remote-tracking branch 'origin/8065-lsp-front-end' into 8065-lsp-front-end 2026-03-19 17:21:36 +01:00
Dmitrii Barnukov 1998bd09c1 dbeaver/pro#5562 update launcher 2026-03-19 17:21:18 +01:00
Sychev Andrey 2d2dbeb79a dbeaver/pro#8065 feat: add LSPConnectionService and module integration 2026-03-19 15:21:42 +01:00
Dmitrii Barnukov 2207546541 dbeaver/pro#5562 update launcher 2026-03-19 14:54:20 +01:00
Sychev Andrey e08d7687aa Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-03-19 10:15:10 +01:00
Sychev Andrey 932eaf8315 Merge branch 'devel' into dbeaver/pro#7844-cb-lsp-server 2026-03-19 10:14:57 +01:00
Dmitrii Barnukov d0a5c1304a dbeaver/pro#7844 switch lsp ws impl 2026-03-12 15:56:02 +01:00
Sychev Andrey e44b3bf2f6 dbeaver/pro#8065 ci: update codemirror dependencies 2026-03-12 10:31:19 +01:00
Sychev Andrey 567cd9ae3b Merge remote-tracking branch 'origin/dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-03-12 10:30:20 +01:00
Dmitrii Barnukov 85d3d834d0 Merge branch 'devel' of github.com:dbeaver/cloudbeaver into dbeaver/pro#7844-cb-lsp-server 2026-03-12 10:20:03 +01:00
Dmitrii Barnukov 09dece1cec Merge branch 'devel' of github.com:dbeaver/cloudbeaver into dbeaver/pro#7844-cb-lsp-server 2026-03-11 15:31:46 +01:00
Dmitrii Barnukov e3c653cd0d Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-03-11 10:11:24 +01:00
Dmitrii Barnukov 48057948c2 dbeaver/pro#7844 changed message handler impl 2026-03-11 09:50:29 +01:00
Dmitrii Barnukov ce7107285d Merge branch 'devel' of github.com:dbeaver/cloudbeaver into dbeaver/pro#7844-cb-lsp-server 2026-03-11 09:26:11 +01:00
Dmitrii Barnukov a43355425f Merge branch 'devel' into dbeaver/pro#7844-cb-lsp-server 2026-03-10 17:19:10 +01:00
Sychev Andrey 4e2e7e0a55 Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-02-19 11:51:15 +01:00
Sychev Andrey 1aea7c19b6 Merge branch 'devel' into dbeaver/pro#7844-cb-lsp-server 2026-02-19 11:45:07 +01:00
Sychev Andrey 144904db24 Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-02-19 11:31:01 +01:00
Dmitrii Barnukov 4d0f1a1a43 dbeaver/pro#7844 added missing export 2026-02-19 11:18:15 +01:00
Sychev Andrey 9c6f6c53c5 Merge branch 'dbeaver/pro#7844-cb-lsp-server' into 8065-lsp-front-end 2026-02-18 10:33:12 +01:00
Sychev Andrey de0958add0 Merge branch 'devel' into dbeaver/pro#7844-cb-lsp-server 2026-02-18 10:23:39 +01:00
Sychev Andrey 7b60aabb08 Merge branch 'devel' into 8065-lsp-front-end 2026-02-18 10:18:26 +01:00
Dmitrii Barnukov e7ac881fab dbeaver/pro#7844 added copy 2026-02-16 09:40:28 +01:00
Dmitrii Barnukov ec9b9de142 dbeaver/pro#7844 refactor deps, added socket endpoint 2026-02-16 09:39:14 +01:00
Dmitrii Barnukov 974f003a23 Merge branch 'devel' of github.com:dbeaver/cloudbeaver into dbeaver/pro#7844-cb-lsp-server 2026-02-13 14:54:13 +01:00
Dmitrii Barnukov c96e383018 dbeaver/pro#7844 added lsp web socket 2026-02-12 17:43:54 +01:00
Sychev Andrey 5db15936f7 dbeaver/pro#8065 feat: implement reconnecting WebSocket transport for LSP client 2026-02-12 14:18:26 +01:00
Sychev Andrey dc0d245d04 Merge branch 'devel' into 8065-lsp-front-end 2026-02-12 10:32:24 +01:00
Sychev Andrey 666982b34d dbeaver/pro#8065 feat: add LSP client support and integrate into SQL editor 2026-02-12 10:30:59 +01:00
28 changed files with 529 additions and 84 deletions
@@ -29,6 +29,8 @@ import io.cloudbeaver.server.servlets.WebStatusServlet;
import io.cloudbeaver.server.websockets.CBEventsLongPollingServlet;
import io.cloudbeaver.server.websockets.CBEventsWebSocket;
import io.cloudbeaver.server.websockets.CBWebSocketServerConfigurator;
import io.cloudbeaver.server.websockets.lsp.LSPWebSocketConstants;
import io.cloudbeaver.server.websockets.lsp.LSPWebSocketEndpoint;
import io.cloudbeaver.service.DBWServiceBindingServlet;
import io.cloudbeaver.service.DBWServiceBindingWebSocket;
import jakarta.websocket.server.ServerEndpointConfig;
@@ -174,13 +176,24 @@ public class CBJettyServer {
JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> {
// Add echo endpoint to server container
ServerEndpointConfig eventWsEnpoint = ServerEndpointConfig.Builder
CBWebSocketServerConfigurator configurator =
new CBWebSocketServerConfigurator(application.getSessionManager());
ServerEndpointConfig eventWsEndpoint = ServerEndpointConfig.Builder
.create(
CBEventsWebSocket.class,
serverConfiguration.getServicesURI() + "ws"
).configurator(new CBWebSocketServerConfigurator(application.getSessionManager()))
).configurator(configurator)
.build();
container.addEndpoint(eventWsEnpoint);
container.addEndpoint(eventWsEndpoint);
ServerEndpointConfig lspWsEndpoint = ServerEndpointConfig.Builder
.create(
LSPWebSocketEndpoint.class,
serverConfiguration.getServicesURI() + LSPWebSocketConstants.ENDPOINT_SUFFIX
).configurator(configurator)
.build();
container.addEndpoint(lspWsEndpoint);
});
JettyUtils.initSessionManager(
@@ -11,6 +11,7 @@ Require-Bundle: org.eclipse.core.runtime;visibility:=reexport,
org.apache.commons.jexl,
org.jkiss.utils;visibility:=reexport,
org.jkiss.dbeaver.model.sql;visibility:=reexport,
org.jkiss.dbeaver.model.lsp,
org.jkiss.dbeaver.data.gis,
org.jkiss.bundle.jetty.server;visibility:=reexport,
com.google.gson;visibility:=reexport,
@@ -19,7 +20,8 @@ Require-Bundle: org.eclipse.core.runtime;visibility:=reexport,
org.jkiss.dbeaver.net.ssh,
io.cloudbeaver.model;visibility:=reexport,
io.cloudbeaver.model.cli,
io.cloudbeaver.service.security;visibility:=reexport
io.cloudbeaver.service.security;visibility:=reexport,
org.eclipse.lsp4j.websocket.jakarta
Export-Package: io.cloudbeaver,
io.cloudbeaver.model,
io.cloudbeaver.model.app,
@@ -33,9 +35,11 @@ Export-Package: io.cloudbeaver,
io.cloudbeaver.server.jobs,
io.cloudbeaver.server.servlets,
io.cloudbeaver.server.websockets,
io.cloudbeaver.server.websockets.lsp,
io.cloudbeaver.service,
io.cloudbeaver.service.navigator,
io.cloudbeaver.service.sql
Import-Package: org.slf4j
Import-Package: org.slf4j,
jakarta.websocket
Bundle-Localization: OSGI-INF/l10n/bundle
Automatic-Module-Name: io.cloudbeaver.server
@@ -30,12 +30,12 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class CBJettyWebSocketManager {
private static final Log log = Log.getLog(CBJettyWebSocketManager.class);
private static final Map<String, List<CBEventsWebSocket>> socketBySessionId = new ConcurrentHashMap<>();
private static final Map<String, List<CBAbstractWebSocket>> socketBySessionId = new ConcurrentHashMap<>();
private CBJettyWebSocketManager() {
}
public static void registerWebSocket(@NotNull String webSessionId, @NotNull CBEventsWebSocket webSocket) {
public static void registerWebSocket(@NotNull String webSessionId, @NotNull CBAbstractWebSocket webSocket) {
socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket);
}
@@ -57,7 +57,7 @@ public class CBJettyWebSocketManager {
entry -> {
var sessionId = entry.getKey();
var webSockets = entry.getValue();
for (CBEventsWebSocket webSocket : webSockets) {
for (CBAbstractWebSocket webSocket : webSockets) {
try {
webSocket.getSession().getBasicRemote().sendPing(
ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8))
@@ -0,0 +1,46 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 DBeaver Corp
*
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of DBeaver Corp and its suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to DBeaver Corp and its suppliers
* and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from DBeaver Corp.
*/
package io.cloudbeaver.server.websockets.lsp;
import io.cloudbeaver.model.session.BaseWebSession;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.app.DBPWorkspace;
import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent;
import org.jkiss.dbeaver.model.lsp.DBLServerSessionProvider;
public class LSPWebServerSessionProvider implements DBLServerSessionProvider {
@NotNull
private final BaseWebSession session;
public LSPWebServerSessionProvider(@NotNull BaseWebSession session) {
this.session = session;
}
@Nullable
@Override
public AbstractSessionPersistent getSession() {
return session;
}
@NotNull
@Override
public DBPWorkspace getWorkspace() {
return session.getWorkspace();
}
}
@@ -0,0 +1,26 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 DBeaver Corp
*
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of DBeaver Corp and its suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to DBeaver Corp and its suppliers
* and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from DBeaver Corp.
*/
package io.cloudbeaver.server.websockets.lsp;
import java.time.Duration;
public class LSPWebSocketConstants {
public static final String ENDPOINT_SUFFIX = "ws/lsp";
public static final Duration IDLE_TIMEOUT = Duration.ofMinutes(5);
}
@@ -0,0 +1,66 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 DBeaver Corp
*
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of DBeaver Corp and its suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to DBeaver Corp and its suppliers
* and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from DBeaver Corp.
*/
package io.cloudbeaver.server.websockets.lsp;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.server.websockets.CBAbstractWebSocket;
import io.cloudbeaver.server.websockets.CBJettyWebSocketManager;
import io.cloudbeaver.server.websockets.CBWebSocketServerConfigurator;
import jakarta.websocket.CloseReason;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.Session;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.websocket.jakarta.WebSocketLauncherBuilder;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.lsp.DBLServer;
public class LSPWebSocketEndpoint extends CBAbstractWebSocket {
private static final Log log = Log.getLog(LSPWebSocketEndpoint.class);
public LSPWebSocketEndpoint() {}
@Override
public void onOpen(Session wsSession, EndpointConfig endpointConfig) {
BaseWebSession webSession = (BaseWebSession) wsSession.getUserProperties()
.get(CBWebSocketServerConfigurator.PROP_WEB_SESSION);
CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this);
wsSession.setMaxIdleTimeout(LSPWebSocketConstants.IDLE_TIMEOUT.toMillis());
wsSession.setMaxTextMessageBufferSize(Integer.MAX_VALUE);
wsSession.setMaxBinaryMessageBufferSize(Integer.MAX_VALUE);
LSPWebServerSessionProvider sessionProvider = new LSPWebServerSessionProvider(webSession);
DBLServer server = new DBLServer(sessionProvider);
Launcher<LanguageClient> launcher = new WebSocketLauncherBuilder<LanguageClient>()
.setSession(wsSession)
.setLocalService(server)
.setRemoteInterface(LanguageClient.class)
.create();
server.connect(launcher.getRemoteProxy());
}
@Override
public void onClose(Session session, CloseReason closeReason) {
log.debug("Closing websocket session: " + session.getId());
}
@Override
public void onError(Session session, Throwable thr) {
log.error("LSP WebSocket error", thr);
}
}
@@ -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<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[],
@@ -1,13 +0,0 @@
query formatSqlQuery(
$projectId: ID!
$connectionId: ID!
$contextId: ID!
$query: String!
) {
query: sqlFormatQuery(
projectId: $projectId
connectionId: $connectionId
contextId: $contextId
query: $query
)
}
@@ -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",
@@ -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';
@@ -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:*",
@@ -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,
@@ -274,6 +274,9 @@
{
"path": "../plugin-sql-editor"
},
{
"path": "../plugin-sql-editor-codemirror"
},
{
"path": "../plugin-sql-editor-navigation-tab"
},
@@ -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",
@@ -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;
}
}
}
@@ -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';
@@ -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);
},
});
@@ -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]);
}
@@ -13,9 +13,15 @@
{
"path": "../core-cli"
},
{
"path": "../core-di"
},
{
"path": "../core-sdk"
},
{
"path": "../core-utils"
},
{
"path": "../plugin-codemirror6"
}
@@ -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))) {
@@ -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,12 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
const sqlDialect = useSqlDialectExtension(data.dialect);
const highlightExtensions = useHighlightExtensions(sqlEditorSettingsService.highlightWhitespace);
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);
}
@@ -60,6 +67,10 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
});
}
if (lspExtension) {
extensions.set(...lspExtension);
}
const dndBox = useDNDBox({
canDrop: context => context.has(DATA_CONTEXT_NAV_NODE),
onDrop: async (context, mouse) => {
@@ -77,6 +77,10 @@ export abstract class BaseSqlDataSource<TDataSource extends QueryDataSource = Qu
return this.executionContext?.projectId ?? null;
}
get lspDocumentUri(): string | null {
return null;
}
readonly icon: string;
readonly history: ISqlDataSourceHistory;
readonly onUpdate: ISyncExecutor;
@@ -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.
@@ -39,6 +39,7 @@ export interface ISqlDataSource<TDataSource extends QueryDataSource = QueryDataS
readonly sourceKey: string;
readonly projectId: string | null;
readonly lspDocumentUri: string | null;
readonly script: string;
readonly cursor: ISqlEditorCursor;
@@ -1,27 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 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 { ConnectionDialectResource, type IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
@injectable(() => [ConnectionDialectResource, NotificationService])
export class SqlDialectInfoService {
constructor(
private readonly connectionDialectResource: ConnectionDialectResource,
private readonly notificationService: NotificationService,
) {}
async formatScript(context: IConnectionExecutionContextInfo, query: string): Promise<string> {
try {
return await this.connectionDialectResource.formatScript(context, query);
} catch (error: any) {
this.notificationService.logException(error, 'Failed to format script');
}
return query;
}
}
@@ -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<void> {
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,
@@ -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';
@@ -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)
+58 -5
View File
@@ -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"
@@ -3953,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:*"
@@ -4115,7 +4117,9 @@ __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:*"
"@cloudbeaver/tsconfig": "workspace:*"
"@types/react": "npm:^19"
@@ -4735,7 +4739,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 +4846,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 +4860,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 +4871,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"
@@ -4891,7 +4911,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:
@@ -6913,7 +6933,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:
@@ -14912,6 +14932,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"
@@ -19701,6 +19730,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"