From 0078e3fc1c3959079b7a927adf12b72dd3184ba8 Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Thu, 26 Mar 2026 16:39:32 +0000 Subject: [PATCH 1/6] Add Hover for Variables --- client/src/services/context.ts | 24 ++++++++++--------- client/src/services/events.ts | 6 +++-- client/src/services/hover.ts | 43 ++++++++++++++++++++++++++-------- client/src/utils/utils.ts | 5 +++- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/client/src/services/context.ts b/client/src/services/context.ts index 0872d57..932e7a5 100644 --- a/client/src/services/context.ts +++ b/client/src/services/context.ts @@ -5,21 +5,23 @@ import { getOriginalVariableName } from "../utils/utils"; export function handleContext(context: LJContext) { extension.context = context; - updateContextForSelection(extension.currentSelection); + if (!extension.file || !extension.currentSelection) return; + + // update variables based on new context in current selection + const { allVars, visibleVars } = getSelectionContextVariables(extension.file, extension.currentSelection); + extension.context.visibleVars = visibleVars; + extension.context.allVars = allVars; extension.webview.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); } -export function updateContextForSelection(selection: Range) { - if (!selection) return; - - const globalVars = extension.context.globalVars || []; - const localVars = extension.context.localVars || []; - const variablesInScope = getVariablesInScope(localVars, extension.file, selection); - const visibleVarsByPosition = getVisibleVariables(variablesInScope, extension.file, selection, false); - const visibleVarsByAnnotationPosition = getVisibleVariables(variablesInScope, extension.file, selection, true); +export function getSelectionContextVariables(file: string, selection: Range): { visibleVars: LJVariable[]; allVars: LJVariable[] } { + const globalVars = extension.context?.globalVars || []; + const localVars = extension.context?.localVars || []; + const variablesInScope = getVariablesInScope(localVars, file, selection); + const visibleVarsByPosition = getVisibleVariables(variablesInScope, file, selection, false); + const visibleVarsByAnnotationPosition = getVisibleVariables(variablesInScope, file, selection, true); const allVars = sortVariables(normalizeVariableRefinements([...globalVars, ...visibleVarsByPosition])); - extension.context.visibleVars = visibleVarsByAnnotationPosition; - extension.context.allVars = allVars; + return { visibleVars: visibleVarsByAnnotationPosition, allVars }; } function getVariablesInScope(variables: LJVariable[], file: string, selection: Range): LJVariable[] { diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 7306327..4483bf2 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { extension } from '../state'; import { updateStateMachine } from './state-machine'; import { SELECTION_DEBOUNCE_MS } from '../utils/constants'; -import { normalizeRange, updateContextForSelection } from './context'; +import { getSelectionContextVariables, normalizeRange } from './context'; import { normalizeFilePath } from '../utils/utils'; import { Range } from '../types/context'; import { updateErrorAtCursor } from './diagnostics'; @@ -68,8 +68,10 @@ function handleContextUpdate(selection: vscode.Selection) { colEnd: selection.end.character }; const normalizedRange = normalizeRange(range); + const { allVars, visibleVars } = getSelectionContextVariables(extension.file, normalizedRange); extension.currentSelection = normalizedRange; - updateContextForSelection(normalizedRange); + extension.context.visibleVars = visibleVars; + 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/hover.ts b/client/src/services/hover.ts index bf9938f..4968e3d 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -1,5 +1,8 @@ import * as vscode from 'vscode'; import { extension } from '../state'; +import type { Range, LJVariable } from '../types/context'; +import { getSelectionContextVariables, rangesIntersect } from './context'; +import { getOriginalVariableName, normalizeFilePath } from '../utils/utils'; /** * Initializes hover provider for LiquidJava diagnostics @@ -7,19 +10,39 @@ import { extension } from '../state'; export function registerHover() { vscode.languages.registerHoverProvider('java', { provideHover(document, position) { - // if webview is visible, do not show hover - if (extension.webview?.isVisible()) return null; - - // get lj diagnostic at the current position - const diagnostics = vscode.languages.getDiagnostics(document.uri); - const diagnostic = diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava'); - if (!diagnostic) return null; - - // create hover content with link to open webview const hoverContent = new vscode.MarkdownString(); hoverContent.isTrusted = true; - hoverContent.appendMarkdown(`\n\n[Open LiquidJava view](command:liquidjava.showView) for more details.`); + + const variable = getHoveredVariable(document, position); + if (variable && variable.mainRefinement && variable.mainRefinement !== 'true') + hoverContent.appendCodeblock(`@Refinement(${JSON.stringify(variable.mainRefinement)})`, 'java'); + + const diagnostics = vscode.languages.getDiagnostics(document.uri); + const containsDiagnostic = !!diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava'); + if (containsDiagnostic) { + if (hoverContent.value.length > 0) hoverContent.appendMarkdown(`\n\n`); + hoverContent.appendMarkdown(`[Open LiquidJava view](command:liquidjava.showView) for more details.`); + } + if (hoverContent.value.length === 0) return null; return new vscode.Hover(hoverContent); } }); } + +function getHoveredVariable(document: vscode.TextDocument, position: vscode.Position): LJVariable | null { + if (!extension.context) return null; + + const wordRange = document.getWordRangeAtPosition(position, /[#]?[A-Za-z_][A-Za-z0-9_#]*/); + if (!wordRange) return null; + + const hoveredWord = document.getText(wordRange); + const file = normalizeFilePath(document.uri.fsPath); + const hoveredRange: Range = { + lineStart: wordRange.start.line, + colStart: wordRange.start.character, + lineEnd: wordRange.end.line, + colEnd: wordRange.end.character + }; + const { allVars } = getSelectionContextVariables(file, hoveredRange); + return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); +} diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index 14b233b..109686b 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -130,7 +130,10 @@ export function getSimpleName(qualifiedName: string): string { } export function getOriginalVariableName(name: string): string { - return name.split("_")[0].replace(/^#/, ''); + if (name.startsWith("this#")) return name.replace(/^this#/, ''); + if (name.startsWith("#")) return name.split("_")[0].replace(/^#/, ''); + return name; + } export function normalizeFilePath(fsPath: string): string { From 12055ea73e54d4f83f5f320dd23e057afbbf139b Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Thu, 26 Mar 2026 16:48:30 +0000 Subject: [PATCH 2/6] Minor Improvement --- client/src/services/events.ts | 11 ++--------- client/src/services/hover.ts | 9 ++------- client/src/utils/utils.ts | 13 ++++++++++++- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 4483bf2..c5a9398 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -3,8 +3,7 @@ import { extension } from '../state'; import { updateStateMachine } from './state-machine'; import { SELECTION_DEBOUNCE_MS } from '../utils/constants'; import { getSelectionContextVariables, normalizeRange } from './context'; -import { normalizeFilePath } from '../utils/utils'; -import { Range } from '../types/context'; +import { normalizeFilePath, toRange } from '../utils/utils'; import { updateErrorAtCursor } from './diagnostics'; let selectionTimeout: NodeJS.Timeout | null = null; @@ -61,13 +60,7 @@ export async function onSelectionChange(event: vscode.TextEditorSelectionChangeE */ function handleContextUpdate(selection: vscode.Selection) { if (!extension.file || !extension.context) return; - const range: Range = { - lineStart: selection.start.line, - colStart: selection.start.character, - lineEnd: selection.end.line, - colEnd: selection.end.character - }; - const normalizedRange = normalizeRange(range); + const normalizedRange = normalizeRange(toRange(selection)); const { allVars, visibleVars } = getSelectionContextVariables(extension.file, normalizedRange); extension.currentSelection = normalizedRange; extension.context.visibleVars = visibleVars; diff --git a/client/src/services/hover.ts b/client/src/services/hover.ts index 4968e3d..1dbca17 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { extension } from '../state'; import type { Range, LJVariable } from '../types/context'; import { getSelectionContextVariables, rangesIntersect } from './context'; -import { getOriginalVariableName, normalizeFilePath } from '../utils/utils'; +import { getOriginalVariableName, normalizeFilePath, toRange } from '../utils/utils'; /** * Initializes hover provider for LiquidJava diagnostics @@ -37,12 +37,7 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi const hoveredWord = document.getText(wordRange); const file = normalizeFilePath(document.uri.fsPath); - const hoveredRange: Range = { - lineStart: wordRange.start.line, - colStart: wordRange.start.character, - lineEnd: wordRange.end.line, - colEnd: wordRange.end.character - }; + const hoveredRange: Range = toRange(wordRange); const { allVars } = getSelectionContextVariables(file, hoveredRange); return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); } diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index 109686b..d9f1d5f 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -2,7 +2,9 @@ import * as fs from "fs"; import * as path from "path"; import * as net from "net"; import * as child_process from "child_process"; +import * as vscode from "vscode"; import { JAVA_BINARY } from "./constants"; +import { Range } from "../types/context"; /** * Finds the Java executable in the system, either in JAVA_HOME or in PATH @@ -139,4 +141,13 @@ export function getOriginalVariableName(name: string): string { export function normalizeFilePath(fsPath: string): string { // uppercase windows drive letter (c:\ -> C:\) return path.normalize(fsPath).replace(/^([a-z]):\\/, (_, drive) => drive.toUpperCase() + ':\\'); -} \ No newline at end of file +} + +export function toRange(selection: vscode.Selection | vscode.Range): Range { + return { + lineStart: selection.start.line, + colStart: selection.start.character, + lineEnd: selection.end.line, + colEnd: selection.end.character + }; +} From f5bb722b9db2e0b6fefd01057f2be4eda815dbba Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Fri, 27 Mar 2026 14:46:29 +0000 Subject: [PATCH 3/6] Fix Positions --- client/src/services/events.ts | 11 +++++++++-- client/src/services/hover.ts | 11 ++++++++--- client/src/utils/utils.ts | 9 --------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/client/src/services/events.ts b/client/src/services/events.ts index c5a9398..8ea9503 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -3,8 +3,9 @@ import { extension } from '../state'; import { updateStateMachine } from './state-machine'; import { SELECTION_DEBOUNCE_MS } from '../utils/constants'; import { getSelectionContextVariables, normalizeRange } from './context'; -import { normalizeFilePath, toRange } from '../utils/utils'; +import { normalizeFilePath } from '../utils/utils'; import { updateErrorAtCursor } from './diagnostics'; +import type { Range } from '../types/context'; let selectionTimeout: NodeJS.Timeout | null = null; @@ -60,7 +61,13 @@ export async function onSelectionChange(event: vscode.TextEditorSelectionChangeE */ function handleContextUpdate(selection: vscode.Selection) { if (!extension.file || !extension.context) return; - const normalizedRange = normalizeRange(toRange(selection)); + const range: Range = { + lineStart: selection.start.line, + colStart: selection.start.character, + lineEnd: selection.end.line, + colEnd: selection.end.character + } + const normalizedRange = normalizeRange(range); const { allVars, visibleVars } = getSelectionContextVariables(extension.file, normalizedRange); extension.currentSelection = normalizedRange; extension.context.visibleVars = visibleVars; diff --git a/client/src/services/hover.ts b/client/src/services/hover.ts index 1dbca17..0c53bcf 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import { extension } from '../state'; import type { Range, LJVariable } from '../types/context'; -import { getSelectionContextVariables, rangesIntersect } from './context'; -import { getOriginalVariableName, normalizeFilePath, toRange } from '../utils/utils'; +import { getSelectionContextVariables } from './context'; +import { getOriginalVariableName, normalizeFilePath } from '../utils/utils'; /** * Initializes hover provider for LiquidJava diagnostics @@ -37,7 +37,12 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi const hoveredWord = document.getText(wordRange); const file = normalizeFilePath(document.uri.fsPath); - const hoveredRange: Range = toRange(wordRange); + const hoveredRange: Range = { + lineStart: position.line + 1, + colStart: position.character + 1, + lineEnd: position.line + 1, + colEnd: position.character + 1 + } const { allVars } = getSelectionContextVariables(file, hoveredRange); return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); } diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index d9f1d5f..ac24b20 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -142,12 +142,3 @@ export function normalizeFilePath(fsPath: string): string { // uppercase windows drive letter (c:\ -> C:\) return path.normalize(fsPath).replace(/^([a-z]):\\/, (_, drive) => drive.toUpperCase() + ':\\'); } - -export function toRange(selection: vscode.Selection | vscode.Range): Range { - return { - lineStart: selection.start.line, - colStart: selection.start.character, - lineEnd: selection.end.line, - colEnd: selection.end.character - }; -} From 3220f5b6d0e1bdceccbc65308bcbc0f6ef63524b Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Fri, 27 Mar 2026 15:00:59 +0000 Subject: [PATCH 4/6] Revert --- client/src/services/events.ts | 11 +++-------- client/src/services/hover.ts | 9 ++------- client/src/utils/utils.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 8ea9503..e7edd91 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -3,7 +3,7 @@ import { extension } from '../state'; import { updateStateMachine } from './state-machine'; import { SELECTION_DEBOUNCE_MS } from '../utils/constants'; import { getSelectionContextVariables, normalizeRange } from './context'; -import { normalizeFilePath } from '../utils/utils'; +import { normalizeFilePath, toRange } from '../utils/utils'; import { updateErrorAtCursor } from './diagnostics'; import type { Range } from '../types/context'; @@ -61,13 +61,8 @@ export async function onSelectionChange(event: vscode.TextEditorSelectionChangeE */ function handleContextUpdate(selection: vscode.Selection) { if (!extension.file || !extension.context) return; - const range: Range = { - lineStart: selection.start.line, - colStart: selection.start.character, - lineEnd: selection.end.line, - colEnd: selection.end.character - } - const normalizedRange = normalizeRange(range); + + const normalizedRange = normalizeRange(toRange(selection)); const { allVars, visibleVars } = getSelectionContextVariables(extension.file, normalizedRange); extension.currentSelection = normalizedRange; extension.context.visibleVars = visibleVars; diff --git a/client/src/services/hover.ts b/client/src/services/hover.ts index 0c53bcf..97ade1e 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { extension } from '../state'; import type { Range, LJVariable } from '../types/context'; import { getSelectionContextVariables } from './context'; -import { getOriginalVariableName, normalizeFilePath } from '../utils/utils'; +import { getOriginalVariableName, normalizeFilePath, toRange } from '../utils/utils'; /** * Initializes hover provider for LiquidJava diagnostics @@ -37,12 +37,7 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi const hoveredWord = document.getText(wordRange); const file = normalizeFilePath(document.uri.fsPath); - const hoveredRange: Range = { - lineStart: position.line + 1, - colStart: position.character + 1, - lineEnd: position.line + 1, - colEnd: position.character + 1 - } + const hoveredRange = toRange(wordRange); const { allVars } = getSelectionContextVariables(file, hoveredRange); return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); } diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index ac24b20..093c8be 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -142,3 +142,12 @@ export function normalizeFilePath(fsPath: string): string { // uppercase windows drive letter (c:\ -> C:\) return path.normalize(fsPath).replace(/^([a-z]):\\/, (_, drive) => drive.toUpperCase() + ':\\'); } + +export function toRange(selection: vscode.Selection | vscode.Range): Range { + return { + lineStart: selection.start.line, + colStart: selection.start.character, + lineEnd: selection.end.line, + colEnd: selection.end.character + }; +} \ No newline at end of file From e9ccb4af8f25e37df908418768f3369a447e6736 Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Mon, 30 Mar 2026 15:18:49 +0100 Subject: [PATCH 5/6] Fix Positions --- client/src/services/hover.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/services/hover.ts b/client/src/services/hover.ts index 97ade1e..70b06e1 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -37,7 +37,14 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi const hoveredWord = document.getText(wordRange); const file = normalizeFilePath(document.uri.fsPath); - const hoveredRange = toRange(wordRange); - const { allVars } = getSelectionContextVariables(file, hoveredRange); + + // we need to use single point cursor position after variable to get all variables until that point + const positionAfterVariable = { + lineStart: wordRange.end.line, + colStart: wordRange.end.character, + lineEnd: wordRange.end.line, + colEnd: wordRange.end.character + }; + const { allVars } = getSelectionContextVariables(file, positionAfterVariable); return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); } From 93d06ecf9b9d94161abd2655f956b96b81d58fce Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Tue, 31 Mar 2026 12:05:00 +0100 Subject: [PATCH 6/6] Minor Change --- client/src/services/hover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/services/hover.ts b/client/src/services/hover.ts index 70b06e1..7b460eb 100644 --- a/client/src/services/hover.ts +++ b/client/src/services/hover.ts @@ -15,7 +15,7 @@ export function registerHover() { const variable = getHoveredVariable(document, position); if (variable && variable.mainRefinement && variable.mainRefinement !== 'true') - hoverContent.appendCodeblock(`@Refinement(${JSON.stringify(variable.mainRefinement)})`, 'java'); + hoverContent.appendCodeblock(`@Refinement("${variable.mainRefinement}")`, 'java'); const diagnostics = vscode.languages.getDiagnostics(document.uri); const containsDiagnostic = !!diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava');