Skip to content

Commit 7b2a8c0

Browse files
authored
Methods Hover (#75)
1 parent 4d18cec commit 7b2a8c0

8 files changed

Lines changed: 163 additions & 7 deletions

File tree

client/src/services/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getOriginalVariableName } from "../utils/utils";
55

66
export function handleContext(context: LJContext) {
77
extension.context = context;
8+
extension.logger?.client.info(JSON.stringify(context.methods, null, 2))
89
if (!extension.file || !extension.currentSelection) return;
910

1011
// update variables based on new context in current selection

client/src/services/definition.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as vscode from 'vscode';
2+
3+
type Definition = {
4+
uri: vscode.Uri;
5+
}
6+
7+
export async function getDefinitions(document: vscode.TextDocument, position: vscode.Position): Promise<Definition[]> {
8+
try {
9+
const definitions = await vscode.commands.executeCommand<(vscode.Location | vscode.LocationLink)[]>(
10+
'vscode.executeDefinitionProvider',
11+
document.uri,
12+
position
13+
) || [];
14+
return definitions.map(definition => definition instanceof vscode.Location
15+
? { uri: definition.uri }
16+
: { uri: definition.targetUri }
17+
);
18+
} catch {
19+
return [];
20+
}
21+
}
22+
23+
export function definitionMatchesClass(definition: Definition, targetClass: string): boolean {
24+
const uri = definition.uri.toString();
25+
return !!targetClass && (uri.includes(targetClass) || uri.includes(targetClass.replace(/\./g, '/')));
26+
}

client/src/services/hover.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import * as vscode from 'vscode';
22
import { extension } from '../state';
3-
import type { Range, LJVariable } from '../types/context';
3+
import type { LJMethod, LJVariable } from '../types/context';
44
import { getSelectionContextVariables } from './context';
5-
import { getOriginalVariableName, normalizeFilePath, toRange } from '../utils/utils';
5+
import { getOriginalVariableName, normalizeFilePath } from '../utils/utils';
6+
import { definitionMatchesClass, getDefinitions } from './definition';
67

78
/**
89
* Initializes hover provider for LiquidJava diagnostics
910
*/
1011
export function registerHover() {
1112
vscode.languages.registerHoverProvider('java', {
12-
provideHover(document, position) {
13+
async provideHover(document, position) {
1314
const hoverContent = new vscode.MarkdownString();
1415
hoverContent.isTrusted = true;
1516

1617
const variable = getHoveredVariable(document, position);
1718
if (variable && variable.mainRefinement && variable.mainRefinement !== 'true')
18-
hoverContent.appendCodeblock(`@Refinement("${variable.mainRefinement}")`, 'java');
19+
hoverContent.appendCodeblock(formatRefinement(variable.mainRefinement), 'java');
20+
else {
21+
const method = await getHoveredMethod(document, position);
22+
if (method) hoverContent.appendCodeblock(formatMethodHover(method), 'java');
23+
}
1924

2025
const diagnostics = vscode.languages.getDiagnostics(document.uri);
2126
const containsDiagnostic = !!diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava');
@@ -48,3 +53,63 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi
4853
const { allVars } = getSelectionContextVariables(file, positionAfterVariable);
4954
return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord);
5055
}
56+
57+
async function getHoveredMethod(document: vscode.TextDocument, position: vscode.Position): Promise<LJMethod | null> {
58+
if (!extension.context) return null;
59+
60+
const wordRange = document.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/);
61+
if (!wordRange) return null;
62+
63+
const hoveredWord = document.getText(wordRange);
64+
const file = normalizeFilePath(document.uri.fsPath);
65+
const methods = extension.context.methods.filter(method => methodNameMatches(method, hoveredWord));
66+
67+
const definitions = await getDefinitions(document, position);
68+
const resolvedMethod = methods.find(method => definitions.some(definition => definitionMatchesClass(definition, method.targetClass)));
69+
if (resolvedMethod) return resolvedMethod;
70+
71+
const receiver = document.lineAt(wordRange.start.line).text
72+
.slice(0, wordRange.start.character)
73+
.match(/([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*$/)?.[1];
74+
if (!receiver) return null;
75+
const receiverVariable = [...(extension.context?.globalVars || []), ...(extension.context?.localVars || [])]
76+
.find(variable =>
77+
getOriginalVariableName(variable.name) === receiver &&
78+
(!variable.position || variable.position.file === file && isBefore(variable.position, wordRange.start))
79+
);
80+
if (!receiverVariable) return null;
81+
82+
return methods.find(method => typeMatchesTargetClass(receiverVariable.type, method.targetClass)) || null;
83+
}
84+
85+
function methodNameMatches(method: LJMethod, hoveredWord: string): boolean {
86+
return method.name === hoveredWord || method.name.endsWith(`.${hoveredWord}`);
87+
}
88+
89+
function typeMatchesTargetClass(type: string, targetClass: string): boolean {
90+
return type === targetClass || targetClass.endsWith(`.${type}`) || type.endsWith(`.${targetClass}`);
91+
}
92+
93+
function isBefore(range: { lineStart: number; colStart: number }, position: vscode.Position): boolean {
94+
return range.lineStart < position.line || range.lineStart === position.line && range.colStart < position.character;
95+
}
96+
97+
function formatRefinement(refinement: string): string {
98+
return `@Refinement("${refinement}")`;
99+
}
100+
101+
function formatStateRefinement(from: string | null, to: string | null): string {
102+
return `@StateRefinement(${[from && `from="${from}"`, to && `to="${to}"`].filter(Boolean).join(', ')})`;
103+
}
104+
105+
function formatMethodHover(method: LJMethod): string {
106+
return [
107+
method.returnRefinement && method.returnRefinement !== 'true' && formatRefinement(method.returnRefinement),
108+
...method.parameters
109+
.filter(p => p.mainRefinement && p.mainRefinement !== 'true')
110+
.map(p => `${formatRefinement(p.mainRefinement)} ${p.type} ${p.name}`),
111+
...method.stateRefinements
112+
.filter(s => s.from || s.to)
113+
.map(s => formatStateRefinement(s.from, s.to))
114+
].filter(Boolean).join('\n');
115+
}

client/src/types/context.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,25 @@ export type LJAlias = {
2929
predicate: string;
3030
}
3131

32+
export type LJMethodStateRefinement = {
33+
from: string | null;
34+
to: string | null;
35+
}
36+
37+
export type LJMethod = {
38+
name: string;
39+
targetClass: string;
40+
returnRefinement: string;
41+
parameters: LJVariable[];
42+
stateRefinements: LJMethodStateRefinement[];
43+
}
44+
3245
export type LJContext = {
3346
localVars: LJVariable[];
3447
globalVars: LJVariable[];
3548
ghosts: LJGhost[];
3649
aliases: LJAlias[];
50+
methods: LJMethod[];
3751
visibleVars: LJVariable[]; // variables visible in the current selection
3852
allVars: LJVariable[]; // instance vars + global vars + vars in scope
3953
fileScopes: Record<string, Range[]>; // file -> scopes
@@ -44,4 +58,4 @@ export type Range = {
4458
colStart: number;
4559
lineEnd: number;
4660
colEnd: number;
47-
}
61+
}

server/src/main/java/dtos/context/ContextHistoryDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public record ContextHistoryDTO(
1414
List<VariableDTO> globalVars,
1515
List<GhostDTO> ghosts,
1616
List<AliasDTO> aliases,
17+
List<MethodDTO> methods,
1718
Map<String, List<SourcePositionDTO>> fileScopes
1819
) {
1920
public static String stringifyType(CtTypeReference<?> typeReference) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dtos.context;
2+
3+
import java.util.List;
4+
import java.util.stream.Collectors;
5+
6+
import liquidjava.processor.context.ObjectState;
7+
import liquidjava.processor.context.RefinedFunction;
8+
import liquidjava.rj_language.Predicate;
9+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
10+
11+
/**
12+
* DTO for serializing RefinedFunction instances to JSON.
13+
*/
14+
public record MethodDTO(
15+
String name,
16+
String targetClass,
17+
String returnRefinement,
18+
List<VariableDTO> parameters,
19+
List<StateRefinementDTO> stateRefinements
20+
) {
21+
public static MethodDTO from(RefinedFunction refinedFunction) {
22+
return new MethodDTO(
23+
refinedFunction.getName(),
24+
refinedFunction.getTargetClass(),
25+
format(refinedFunction.getRefReturn()),
26+
refinedFunction.getArguments().stream().map(VariableDTO::from).filter(v -> v != null).collect(Collectors.toList()),
27+
refinedFunction.getAllStates().stream().map(StateRefinementDTO::from).collect(Collectors.toList())
28+
);
29+
}
30+
31+
public record StateRefinementDTO(String from, String to) {
32+
public static StateRefinementDTO from(ObjectState state) {
33+
return new StateRefinementDTO(
34+
state.hasFrom() ? format(state.getFrom()) : null,
35+
state.hasTo() ? format(state.getTo()) : null
36+
);
37+
}
38+
}
39+
40+
private static String format(Predicate predicate) {
41+
return predicate == null ? "" : ExpressionFormatter.format(predicate);
42+
}
43+
}

server/src/main/java/dtos/diagnostics/SourcePositionDTO.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ public record SourcePositionDTO(String file, int lineStart, int colStart, int li
99

1010
public static SourcePositionDTO from(SourcePosition pos) {
1111
if (pos == null) return null;
12-
String file = pos.getFile() != null ? pos.getFile().getAbsolutePath() : null;
13-
return new SourcePositionDTO(file, pos.getLine() - 1, pos.getColumn() - 1, pos.getEndLine() - 1, pos.getEndColumn());
12+
try {
13+
String file = pos.getFile() != null ? pos.getFile().getAbsolutePath() : null;
14+
return new SourcePositionDTO(file, pos.getLine() - 1, pos.getColumn() - 1, pos.getEndLine() - 1, pos.getEndColumn());
15+
} catch (UnsupportedOperationException e) {
16+
return null;
17+
}
1418
}
1519

1620
public static SourcePositionDTO from(String pos) {

server/src/main/java/utils/ContextHistoryConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import dtos.context.AliasDTO;
1010
import dtos.context.ContextHistoryDTO;
11+
import dtos.context.MethodDTO;
1112
import dtos.context.GhostDTO;
1213
import dtos.context.VariableDTO;
1314
import dtos.diagnostics.SourcePositionDTO;
@@ -29,6 +30,7 @@ public static ContextHistoryDTO convertToDTO(ContextHistory contextHistory) {
2930
contextHistory.getGlobalVars().stream().map(VariableDTO::from).filter(v -> v != null).collect(Collectors.toList()),
3031
contextHistory.getGhosts().stream().map(GhostDTO::from).collect(Collectors.toList()),
3132
contextHistory.getAliases().stream().map(AliasDTO::from).collect(Collectors.toList()),
33+
contextHistory.getMethods().stream().map(MethodDTO::from).filter(f -> f != null).collect(Collectors.toList()),
3234
parseFileScopes(contextHistory.getFileScopes())
3335
);
3436
}

0 commit comments

Comments
 (0)