From bd7126270bfb52b23fecafbfc062899e198c186b Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Sun, 2 Aug 2026 17:22:12 +0100 Subject: [PATCH] Fix Autocomplete --- client/src/lsp/client.ts | 12 +++++-- client/src/services/annotation.ts | 43 +++++++++++++++++++++++++ client/src/services/autocomplete.ts | 49 +++++++++++------------------ client/src/services/events.ts | 4 ++- client/src/services/status-bar.ts | 6 ++-- 5 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 client/src/services/annotation.ts diff --git a/client/src/lsp/client.ts b/client/src/lsp/client.ts index 99aa0c9f..998d7c2b 100644 --- a/client/src/lsp/client.ts +++ b/client/src/lsp/client.ts @@ -8,6 +8,7 @@ import { onActiveFileChange } from '../services/events'; import type { LJDiagnostic } from "../types/diagnostics"; import { LJContext } from '../types/context'; import { handleContext } from '../services/context'; +import { isCursorInsideLiquidJavaAnnotation } from '../services/annotation'; /** * Starts the client and connects it to the language server @@ -29,6 +30,13 @@ export async function runClient(context: vscode.ExtensionContext, port: number) }; const clientOptions: LanguageClientOptions = { documentSelector: [{ language: "java" }], + middleware: { + didSave: async (document, next) => { + // skip verification if the cursor is inside a LiquidJava annotation + if (isCursorInsideLiquidJavaAnnotation(document)) return; + await next(document); + }, + }, }; extension.client = new LanguageClient("liquidJavaServer", "LiquidJava Server", serverOptions, clientOptions); @@ -62,8 +70,8 @@ export async function runClient(context: vscode.ExtensionContext, port: number) // update status bar on file save context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(() => { - if (extension.client) { + vscode.workspace.onDidSaveTextDocument(document => { + if (extension.client && !isCursorInsideLiquidJavaAnnotation(document)) { updateStatusBar("loading"); } }) diff --git a/client/src/services/annotation.ts b/client/src/services/annotation.ts new file mode 100644 index 00000000..0c34e028 --- /dev/null +++ b/client/src/services/annotation.ts @@ -0,0 +1,43 @@ +import * as vscode from "vscode"; +import { LIQUIDJAVA_ANNOTATION_START, LJAnnotation } from "../utils/constants"; + +/** + * Returns the LiquidJava annotation containing the given position + */ +export function getActiveLiquidJavaAnnotation(document: vscode.TextDocument, position: vscode.Position): LJAnnotation | null { + const textUntilCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position)); + LIQUIDJAVA_ANNOTATION_START.lastIndex = 0; + let match: RegExpExecArray | null = null; + let lastAnnotationStart = -1; + let lastAnnotationName: LJAnnotation | null = null; + while ((match = LIQUIDJAVA_ANNOTATION_START.exec(textUntilCursor)) !== null) { + lastAnnotationStart = match.index; + lastAnnotationName = match[2] ? match[2] as LJAnnotation : null; + } + if (lastAnnotationStart === -1 || !lastAnnotationName) return null; + + const fromLastAnnotation = textUntilCursor.slice(lastAnnotationStart); + let parenthesisDepth = 0; + let isInsideString = false; + for (let i = 0; i < fromLastAnnotation.length; i++) { + const char = fromLastAnnotation[i]; + const previousChar = i > 0 ? fromLastAnnotation[i - 1] : ""; + if (char === '"' && previousChar !== "\\") { + isInsideString = !isInsideString; + continue; + } + if (isInsideString) continue; + if (char === "(") parenthesisDepth++; + if (char === ")") parenthesisDepth--; + } + return parenthesisDepth > 0 ? lastAnnotationName : null; +} + +/** + * Checks whether any cursor in the active editor is inside a LiquidJava annotation + */ +export function isCursorInsideLiquidJavaAnnotation(document: vscode.TextDocument): boolean { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.uri.toString() !== document.uri.toString()) return false; + return editor.selections.some(selection => Boolean(getActiveLiquidJavaAnnotation(document, selection.active))); +} diff --git a/client/src/services/autocomplete.ts b/client/src/services/autocomplete.ts index f63cb6ad..649ff30e 100644 --- a/client/src/services/autocomplete.ts +++ b/client/src/services/autocomplete.ts @@ -2,9 +2,10 @@ import * as vscode from "vscode"; import { extension } from "../state"; import type { LJVariable, LJContext, LJGhost, LJAlias } from "../types/context"; import { getSimpleName } from "../utils/utils"; -import { LIQUIDJAVA_ANNOTATION_START, LJAnnotation } from "../utils/constants"; +import { LJAnnotation } from "../utils/constants"; import { filterDuplicateVariables, filterInstanceVariables } from "./context"; import { isExtensionRunning } from "../extension"; +import { getActiveLiquidJavaAnnotation } from "./annotation"; type CompletionItemOptions = { name: string; @@ -43,7 +44,22 @@ export function registerAutocomplete(context: vscode.ExtensionContext) { }); return Array.from(uniqueItems.values()); }, - }, '.', '"') + }, '.', '"'), + vscode.workspace.onDidChangeTextDocument(event => { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.uri.toString() !== event.document.uri.toString()) return; + if (event.document.languageId !== "java" || event.contentChanges.length === 0) return; + + // VS Code does not reliably invoke completion providers while editing string literals + // wait for selection to catch up with the document change and then retrigger completion + setTimeout(() => { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor || activeEditor.document.uri.toString() !== event.document.uri.toString()) return; + if (!isExtensionRunning()) return; + if (!getActiveLiquidJavaAnnotation(activeEditor.document, activeEditor.selection.active)) return; + void vscode.commands.executeCommand("editor.action.triggerSuggest"); + }, 0); + }) ); } @@ -212,35 +228,6 @@ function createCompletionItem({ name, kind, labelDetail, description, detail, do return item; } -function getActiveLiquidJavaAnnotation(document: vscode.TextDocument, position: vscode.Position): LJAnnotation | null { - const textUntilCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position)); - LIQUIDJAVA_ANNOTATION_START.lastIndex = 0; - let match: RegExpExecArray | null = null; - let lastAnnotationStart = -1; - let lastAnnotationName: LJAnnotation | null = null; - while ((match = LIQUIDJAVA_ANNOTATION_START.exec(textUntilCursor)) !== null) { - lastAnnotationStart = match.index; - lastAnnotationName = match[2] ? match[2] as LJAnnotation : null; - } - if (lastAnnotationStart === -1 || !lastAnnotationName) return null; - - const fromLastAnnotation = textUntilCursor.slice(lastAnnotationStart); - let parenthesisDepth = 0; - let isInsideString = false; - for (let i = 0; i < fromLastAnnotation.length; i++) { - const char = fromLastAnnotation[i]; - const previousChar = i > 0 ? fromLastAnnotation[i - 1] : ""; - if (char === '"' && previousChar !== "\\") { - isInsideString = !isInsideString; - continue; - } - if (isInsideString) continue; - if (char === "(") parenthesisDepth++; - if (char === ")") parenthesisDepth--; - } - return parenthesisDepth > 0 ? lastAnnotationName : null; -} - function getReceiverBeforeDot(document: vscode.TextDocument, position: vscode.Position): string | null { const prefix = document.lineAt(position.line).text.slice(0, position.character); const match = prefix.match(/((?:old\s*\(\s*this\s*\))|(?:[A-Za-z_]\w*))\.\w*$/); diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 3596a10f..9f566936 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -4,6 +4,7 @@ import { updateStateMachine } from './state-machine'; import { SELECTION_DEBOUNCE_MS } from '../utils/constants'; import { getSelectionContextVariables, normalizeRange, updateErrorAtCursor } from './context'; import { normalizeFilePath, toRange } from '../utils/utils'; +import { isCursorInsideLiquidJavaAnnotation } from './annotation'; let selectionTimeout: NodeJS.Timeout | null = null; @@ -20,6 +21,7 @@ export function registerEvents(context: vscode.ExtensionContext) { }), vscode.workspace.onDidSaveTextDocument(async document => { if (document.uri.scheme !== 'file' || document.languageId !== "java") return; + if (isCursorInsideLiquidJavaAnnotation(document)) return; await updateStateMachine(document) }), vscode.window.onDidChangeTextEditorSelection(event => { @@ -70,4 +72,4 @@ function handleContextUpdate(selection: vscode.Selection) { extension.context.allVars = allVars; updateErrorAtCursor(); extension.webview?.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); -} \ No newline at end of file +} diff --git a/client/src/services/status-bar.ts b/client/src/services/status-bar.ts index 5b19d51d..8ea5cfbe 100644 --- a/client/src/services/status-bar.ts +++ b/client/src/services/status-bar.ts @@ -35,10 +35,10 @@ export function registerStatusBar(context: vscode.ExtensionContext) { * @param notifyWebview Whether the webview should reflect this status update. */ export function updateStatusBar(status: ExtensionStatus, notifyWebview = status !== "loading") { - if (notifyWebview) { - extension.status = status; + extension.status = status; + if (notifyWebview) extension.webview?.sendMessage({ type: "status", status }); - } + const color = status === "stopped" || status === "crashed" ? "errorForeground" : "statusBar.foreground"; if (!extension.statusBar) return; extension.statusBar.color = new vscode.ThemeColor(color);