From 16b437164eb885495b278b9a49e96879fe2f8ad6 Mon Sep 17 00:00:00 2001 From: Vladimir Makarov Date: Sat, 28 Jun 2025 08:52:57 +0100 Subject: [PATCH 1/4] Allow drivers to provide completions from the raw query text and position --- packages/language-server/src/connection.ts | 7 +- .../plugins/intellisense/language-server.ts | 121 ++++++++++-------- packages/types/index.d.ts | 5 + 3 files changed, 81 insertions(+), 52 deletions(-) diff --git a/packages/language-server/src/connection.ts b/packages/language-server/src/connection.ts index 90fa704a7..fde3e9e92 100644 --- a/packages/language-server/src/connection.ts +++ b/packages/language-server/src/connection.ts @@ -1,4 +1,4 @@ -import { NSDatabase, IConnectionDriver, IConnection, MConnectionExplorer, ContextValue, InternalID, IQueryOptions } from '@sqltools/types'; +import { NSDatabase, IConnectionDriver, IConnection, MConnectionExplorer, ContextValue, InternalID, IQueryOptions, IRawCompletionItem } from '@sqltools/types'; import decorateLSException from '@sqltools/util/decorators/ls-decorate-exception'; import { getConnectionId } from '@sqltools/util/connection'; import ConfigRO from '@sqltools/util/config-manager'; @@ -177,4 +177,9 @@ export default class Connection { if (typeof this.conn.getStaticCompletions !== 'function') return Promise.resolve({} as any); return this.conn.getStaticCompletions(); } + + public getCompletionsForRawQuery(text: string, currentOffset: number): Promise { + if (typeof this.conn.getCompletionsForRawQuery !== 'function') return Promise.resolve(null); + return this.conn.getCompletionsForRawQuery(text, currentOffset); + } } diff --git a/packages/plugins/intellisense/language-server.ts b/packages/plugins/intellisense/language-server.ts index 412538a2b..ffa307fff 100644 --- a/packages/plugins/intellisense/language-server.ts +++ b/packages/plugins/intellisense/language-server.ts @@ -107,13 +107,70 @@ export default class IntellisensePlugin implements IL return []; } - private onCompletion: Arg0 = async params => { + private getCompletionsFromHueAst = async ({ currentWord, conn, text, currentOffset }: { currentWord: string; conn: Connection | null; text: string; currentOffset: number }) => { let completionsMap = { query: [], tables: [], columns: [], dbs: [] }; + + const hueAst = sqlAutocompleteParser.parseSql(text.substring(0, currentOffset), text.substring(currentOffset)); + + completionsMap.query = (hueAst.suggestKeywords || []).filter(kw => kw.value.startsWith(currentWord)).map(kw => { + label: kw.value, + detail: kw.value, + filterText: kw.value, + // weights provided by hue are in reversed order + sortText: `${String(10000 - kw.weight).padStart(5, '0')}:${kw.value}`, + kind: CompletionItemKind.Keyword, + documentation: { + value: `\`\`\`yaml\nWORD: ${kw.value}\n\`\`\``, + kind: 'markdown' + } + }) + const visitedKeywords: [string] = (hueAst.suggestKeywords || []).map(kw => kw.value) + if (!conn) { + log.info('no active connection completions count: %d', completionsMap.query.length); + return completionsMap.query; + }; + // Can't distinguish functions types, so put all other keywords + if (hueAst.suggestFunctions || hueAst.suggestAggregateFunctions || hueAst.suggestAnalyticFunctions) { + const staticCompletions = await conn.getStaticCompletions(); + for (let keyword in staticCompletions) { + if (visitedKeywords.includes(keyword)) { + continue; + } + if (!keyword.startsWith(currentWord)) { + continue; + } + visitedKeywords.push(keyword); + const value: NSDatabase.IStaticCompletion = staticCompletions[keyword] + completionsMap.query.push({ + ...value, sortText: `4:${value.label}`, + kind: CompletionItemKind.Function + }); + } + } + + const [tableCompletions, columnCompletions, dbCompletions] = await Promise.all([ + (hueAst.suggestTables != undefined) ? this.getTableCompletions({ currentWord, conn, suggestTables: hueAst.suggestTables }) : [], + (hueAst.suggestColumns != undefined) ? this.getColumnCompletions({ currentWord, conn, suggestColumns: hueAst.suggestColumns }) : [], + (hueAst.suggestDatabases != undefined) ? this.getDatabasesCompletions({ currentWord, conn, suggestDatabases: hueAst.suggestDatabases }) : [], + ]); + completionsMap.tables = tableCompletions; + completionsMap.columns = columnCompletions; + completionsMap.dbs = dbCompletions; + + const completions = completionsMap.columns + .concat(completionsMap.tables) + .concat(completionsMap.dbs) + .concat(completionsMap.query); + + return completions; + } + + private onCompletion: Arg0 = async params => { try { const { currentWord, @@ -122,61 +179,23 @@ export default class IntellisensePlugin implements IL currentOffset } = await this.getQueryData(params); - const hueAst = sqlAutocompleteParser.parseSql(text.substring(0, currentOffset), text.substring(currentOffset)); - - completionsMap.query = (hueAst.suggestKeywords || []).filter(kw => kw.value.startsWith(currentWord)).map(kw => { - label: kw.value, - detail: kw.value, - filterText: kw.value, - // weights provided by hue are in reversed order - sortText: `${String(10000 - kw.weight).padStart(5, '0')}:${kw.value}`, - kind: CompletionItemKind.Keyword, - documentation: { - value: `\`\`\`yaml\nWORD: ${kw.value}\n\`\`\``, - kind: 'markdown' - } - }) - const visitedKeywords: [string] = (hueAst.suggestKeywords || []).map(kw => kw.value) - if (!conn) { - log.info('no active connection completions count: %d', completionsMap.query.length); - return completionsMap.query; - }; - // Can't distinguish functions types, so put all other keywords - if (hueAst.suggestFunctions || hueAst.suggestAggregateFunctions || hueAst.suggestAnalyticFunctions) { - const staticCompletions = await conn.getStaticCompletions(); - for (let keyword in staticCompletions) { - if (visitedKeywords.includes(keyword)) { - continue; - } - if (!keyword.startsWith(currentWord)) { - continue; - } - visitedKeywords.push(keyword); - const value: NSDatabase.IStaticCompletion = staticCompletions[keyword] - completionsMap.query.push({ - ...value, sortText: `4:${value.label}`, - kind: CompletionItemKind.Function - }); - } + // First try to get completions from connection's getCompletionsForRawQuery method + const connectionCompletions = await conn.getCompletionsForRawQuery(text, currentOffset); + if (connectionCompletions !== null) { + log.debug('using connection completions, count: %d', connectionCompletions.length); + return connectionCompletions; } + - const [tableCompletions, columnCompletions, dbCompletions] = await Promise.all([ - (hueAst.suggestTables != undefined) ? this.getTableCompletions({ currentWord, conn, suggestTables: hueAst.suggestTables }) : [], - (hueAst.suggestColumns != undefined) ? this.getColumnCompletions({ currentWord, conn, suggestColumns: hueAst.suggestColumns }) : [], - (hueAst.suggestDatabases != undefined) ? this.getDatabasesCompletions({ currentWord, conn, suggestDatabases: hueAst.suggestDatabases }) : [], - ]); - completionsMap.tables = tableCompletions; - completionsMap.columns = columnCompletions; - completionsMap.dbs = dbCompletions; + // Fallback to hue AST-based completions + log.debug('falling back to hue AST completions'); + const completions = await this.getCompletionsFromHueAst({ currentWord, conn, text, currentOffset }); + log.debug('total completions %d', completions.length); + return completions; } catch (error) { log.error('got an error:\n %O', error); + return []; } - const completions = completionsMap.columns - .concat(completionsMap.tables) - .concat(completionsMap.dbs) - .concat(completionsMap.query); - log.debug('total completions %d', completions.length); - return completions; } public register(server: T) { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 76fc8a023..3211d0bc2 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -284,6 +284,10 @@ export interface IQueryOptions { export interface IConnectionDriverConstructor { new(credentials: IConnection, getWorkspaceFolders?: LSIConnection['workspace']['getWorkspaceFolders']): IConnectionDriver; } +export interface IRawCompletionItem { + // TODO add more fields or maybe reuse existing types + label: string; +} export interface IConnectionDriver { connection: any; credentials: IConnection; @@ -311,6 +315,7 @@ export interface IConnectionDriver { port: number; } ): Promise<{ port: number }>; + getCompletionsForRawQuery?(text: string, currentOffset: number): Promise; } export declare enum ContextValue { From 00ca538dd9ec89196c7217fb06e08e9238099857 Mon Sep 17 00:00:00 2001 From: Vladimir Makarov Date: Sat, 28 Jun 2025 08:42:26 +0000 Subject: [PATCH 2/4] Return raw query completions as CompletionItem[] --- packages/language-server/src/connection.ts | 6 ++-- .../plugins/intellisense/language-server.ts | 15 ++++----- packages/types/index.d.ts | 32 ++++++++----------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/language-server/src/connection.ts b/packages/language-server/src/connection.ts index fde3e9e92..c482cd82d 100644 --- a/packages/language-server/src/connection.ts +++ b/packages/language-server/src/connection.ts @@ -1,10 +1,10 @@ -import { NSDatabase, IConnectionDriver, IConnection, MConnectionExplorer, ContextValue, InternalID, IQueryOptions, IRawCompletionItem } from '@sqltools/types'; +import { NSDatabase, IConnectionDriver, IConnection, MConnectionExplorer, ContextValue, InternalID, IQueryOptions } from '@sqltools/types'; import decorateLSException from '@sqltools/util/decorators/ls-decorate-exception'; import { getConnectionId } from '@sqltools/util/connection'; import ConfigRO from '@sqltools/util/config-manager'; import generateId from '@sqltools/util/internal-id'; import LSContext from './context'; -import { IConnection as LSIconnection } from 'vscode-languageserver'; +import { IConnection as LSIconnection, CompletionItem } from 'vscode-languageserver'; import DriverNotInstalledError from './exception/driver-not-installed'; import { createLogger } from '@sqltools/log/src'; @@ -178,7 +178,7 @@ export default class Connection { return this.conn.getStaticCompletions(); } - public getCompletionsForRawQuery(text: string, currentOffset: number): Promise { + public getCompletionsForRawQuery(text: string, currentOffset: number): Promise { if (typeof this.conn.getCompletionsForRawQuery !== 'function') return Promise.resolve(null); return this.conn.getCompletionsForRawQuery(text, currentOffset); } diff --git a/packages/plugins/intellisense/language-server.ts b/packages/plugins/intellisense/language-server.ts index ffa307fff..776f25c06 100644 --- a/packages/plugins/intellisense/language-server.ts +++ b/packages/plugins/intellisense/language-server.ts @@ -52,7 +52,7 @@ export default class IntellisensePlugin implements IL } } - private getDatabasesCompletions = async ({ currentWord, conn, suggestDatabases }: { conn: Connection; currentWord: string; suggestDatabases: any }) => { + private getDatabasesCompletions = async ({ currentWord, conn, suggestDatabases }: { conn: Connection; currentWord: string; suggestDatabases: any }): Promise => { const prefix = (suggestDatabases.prependQuestionMark ? "? " : "") + (suggestDatabases.prependFrom ? "FROM " : ""); const suffix = suggestDatabases.appendDot ? "." : ""; @@ -70,7 +70,7 @@ export default class IntellisensePlugin implements IL return []; } - private getTableCompletions = async ({ currentWord, conn, suggestTables }: { conn: Connection; currentWord: string; suggestTables: any }) => { + private getTableCompletions = async ({ currentWord, conn, suggestTables }: { conn: Connection; currentWord: string; suggestTables: any }): Promise => { const prefix = (suggestTables.prependQuestionMark ? "? " : "") + (suggestTables.prependFrom ? "FROM " : ""); const database = suggestTables.identifierChain && suggestTables.identifierChain[0].name; const suffix = suggestTables.appendDot ? "." : ""; @@ -89,7 +89,7 @@ export default class IntellisensePlugin implements IL return []; } - private getColumnCompletions = async ({ currentWord, conn, suggestColumns }: { conn: Connection; currentWord: string; suggestColumns: any }) => { + private getColumnCompletions = async ({ currentWord, conn, suggestColumns }: { conn: Connection; currentWord: string; suggestColumns: any }): Promise => { const tables = suggestColumns.tables .map(table => table.identifierChain.map(id => id.name || id.cte)) .map((t: [string]) => ({ label: t.pop(), database: t.pop() })); @@ -107,7 +107,7 @@ export default class IntellisensePlugin implements IL return []; } - private getCompletionsFromHueAst = async ({ currentWord, conn, text, currentOffset }: { currentWord: string; conn: Connection | null; text: string; currentOffset: number }) => { + private getCompletionsFromHueAst = async ({ currentWord, conn, text, currentOffset }: { currentWord: string; conn: Connection | null; text: string; currentOffset: number }): Promise => { let completionsMap = { query: [], tables: [], @@ -166,7 +166,7 @@ export default class IntellisensePlugin implements IL .concat(completionsMap.tables) .concat(completionsMap.dbs) .concat(completionsMap.query); - + return completions; } @@ -182,13 +182,12 @@ export default class IntellisensePlugin implements IL // First try to get completions from connection's getCompletionsForRawQuery method const connectionCompletions = await conn.getCompletionsForRawQuery(text, currentOffset); if (connectionCompletions !== null) { - log.debug('using connection completions, count: %d', connectionCompletions.length); + log.debug('Got completions from raw the query, count: %d', connectionCompletions.length); return connectionCompletions; } - // Fallback to hue AST-based completions - log.debug('falling back to hue AST completions'); + log.debug('Using completions based on hue SQL parser'); const completions = await this.getCompletionsFromHueAst({ currentWord, conn, text, currentOffset }); log.debug('total completions %d', completions.length); return completions; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 3211d0bc2..8d2607dab 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -1,5 +1,5 @@ import { ErrorHandler as LanguageClientErrorHandler, LanguageClient } from 'vscode-languageclient'; -import { IConnection as LSIConnection, TextDocuments } from 'vscode-languageserver'; +import { IConnection as LSIConnection, TextDocuments, CompletionItem } from 'vscode-languageserver'; import { RequestType, RequestType0 } from 'vscode-languageserver-protocol'; export declare namespace NodeJS { @@ -207,7 +207,7 @@ export interface IConnection { * @memberof IConnection */ ssh?: 'Enabled' | 'Disabled'; - + /** * SSH connection options. Required when ssh is 'Enabled' * @type {object} @@ -220,7 +220,7 @@ export interface IConnection { * @memberof IConnection.sshOptions */ host: string; - + /** * SSH port * @type {number} @@ -228,14 +228,14 @@ export interface IConnection { * @memberof IConnection.sshOptions */ port: number; - + /** * SSH username * @type {string} * @memberof IConnection.sshOptions */ username: string; - + /** * SSH password. You can use option askForPassword to prompt password before connect * @type {string} @@ -243,7 +243,7 @@ export interface IConnection { * @memberof IConnection.sshOptions */ password?: string; - + /** * Path to private key file * @type {string} @@ -284,10 +284,6 @@ export interface IQueryOptions { export interface IConnectionDriverConstructor { new(credentials: IConnection, getWorkspaceFolders?: LSIConnection['workspace']['getWorkspaceFolders']): IConnectionDriver; } -export interface IRawCompletionItem { - // TODO add more fields or maybe reuse existing types - label: string; -} export interface IConnectionDriver { connection: any; credentials: IConnection; @@ -315,7 +311,7 @@ export interface IConnectionDriver { port: number; } ): Promise<{ port: number }>; - getCompletionsForRawQuery?(text: string, currentOffset: number): Promise; + getCompletionsForRawQuery?(text: string, currentOffset: number): Promise; } export declare enum ContextValue { @@ -752,13 +748,13 @@ export interface ISettings { */ useNodeRuntime?: null | boolean | string; - /** - * Disable node runtime detection notifications. - * @default false - * @type {boolean} - * @memberof ISettings - */ - disableNodeDetectNotifications?: boolean; + /** + * Disable node runtime detection notifications. + * @default false + * @type {boolean} + * @memberof ISettings + */ + disableNodeDetectNotifications?: boolean; /** * Columns sort order From d8f649f395319995cb9edac21289650f97c90a43 Mon Sep 17 00:00:00 2001 From: Vladimir Makarov Date: Sun, 20 Jul 2025 08:27:50 +0000 Subject: [PATCH 3/4] Return keyword completions when no connection --- .../plugins/intellisense/language-server.ts | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/plugins/intellisense/language-server.ts b/packages/plugins/intellisense/language-server.ts index 776f25c06..5aa45e8c0 100644 --- a/packages/plugins/intellisense/language-server.ts +++ b/packages/plugins/intellisense/language-server.ts @@ -107,17 +107,12 @@ export default class IntellisensePlugin implements IL return []; } - private getCompletionsFromHueAst = async ({ currentWord, conn, text, currentOffset }: { currentWord: string; conn: Connection | null; text: string; currentOffset: number }): Promise => { - let completionsMap = { - query: [], - tables: [], - columns: [], - dbs: [] - }; - - const hueAst = sqlAutocompleteParser.parseSql(text.substring(0, currentOffset), text.substring(currentOffset)); + private getHueAst(text: string, currentOffset:number) { + return sqlAutocompleteParser.parseSql(text.substring(0, currentOffset), text.substring(currentOffset)); + } - completionsMap.query = (hueAst.suggestKeywords || []).filter(kw => kw.value.startsWith(currentWord)).map(kw => { + private getKeywordsCompletion(hueAst: any, currentWord: string): CompletionItem[] { + return (hueAst.suggestKeywords || []).filter(kw => kw.value.startsWith(currentWord)).map(kw => { label: kw.value, detail: kw.value, filterText: kw.value, @@ -129,11 +124,21 @@ export default class IntellisensePlugin implements IL kind: 'markdown' } }) - const visitedKeywords: [string] = (hueAst.suggestKeywords || []).map(kw => kw.value) - if (!conn) { - log.info('no active connection completions count: %d', completionsMap.query.length); - return completionsMap.query; + } + + private getCompletionsFromHueAst = async ({ currentWord, conn, text, currentOffset }: { currentWord: string; conn: Connection | null; text: string; currentOffset: number }): Promise => { + let completionsMap = { + query: [], + tables: [], + columns: [], + dbs: [] }; + + const hueAst = this.getHueAst(text, currentOffset); + + completionsMap.query = this.getKeywordsCompletion(hueAst, currentWord); + const visitedKeywords: [string] = (hueAst.suggestKeywords || []).map(kw => kw.value) + // Can't distinguish functions types, so put all other keywords if (hueAst.suggestFunctions || hueAst.suggestAggregateFunctions || hueAst.suggestAnalyticFunctions) { const staticCompletions = await conn.getStaticCompletions(); @@ -179,17 +184,26 @@ export default class IntellisensePlugin implements IL currentOffset } = await this.getQueryData(params); - // First try to get completions from connection's getCompletionsForRawQuery method + // When no active connection attatched to the file - return only SQL keywords + if (!conn) { + const hueAst = this.getHueAst(text, currentOffset); + const keywordCompletions = this.getKeywordsCompletion(hueAst, currentWord); + + log.info('no active connection, keyword completions:: %d', keywordCompletions.length); + return keywordCompletions; + }; + + // First try connection's getCompletionsForRawQuery method if the connection supports it const connectionCompletions = await conn.getCompletionsForRawQuery(text, currentOffset); if (connectionCompletions !== null) { - log.debug('Got completions from raw the query, count: %d', connectionCompletions.length); + log.info('Got completions from raw the query, count: %d', connectionCompletions.length); return connectionCompletions; } // Fallback to hue AST-based completions - log.debug('Using completions based on hue SQL parser'); + log.info('Using completions based on hue SQL parser'); const completions = await this.getCompletionsFromHueAst({ currentWord, conn, text, currentOffset }); - log.debug('total completions %d', completions.length); + log.info('total completions %d', completions.length); return completions; } catch (error) { log.error('got an error:\n %O', error); From d7bdf3ac719959b3e3dc72eaa9893a2be027af13 Mon Sep 17 00:00:00 2001 From: Vladimir Makarov Date: Sun, 20 Jul 2025 08:46:06 +0000 Subject: [PATCH 4/4] Add comments to the new IConnectionDriver interface method --- packages/types/index.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 8d2607dab..eb1c9c945 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -311,6 +311,11 @@ export interface IConnectionDriver { port: number; } ): Promise<{ port: number }>; + /** + * If implemented, will be used to provide completions based on the provided text and position. + * @param text The full query text + * @param currentOffset The position in the query where the completion is requested. + */ getCompletionsForRawQuery?(text: string, currentOffset: number): Promise; }