Skip to content

Commit 68bc433

Browse files
authored
Autocomplete Improvements (#63)
1 parent e21b55f commit 68bc433

File tree

12 files changed

+150
-67
lines changed

12 files changed

+150
-67
lines changed

client/src/extension.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,14 @@ import { runClient, stopClient } from "./lsp/client";
1818
*/
1919
export async function activate(context: vscode.ExtensionContext) {
2020
registerLogger(context);
21+
extension.logger.client.info("Activating LiquidJava extension...");
22+
2123
registerStatusBar(context);
2224
registerCommands(context);
23-
registerWebview(context);
2425
registerEvents(context);
26+
registerWebview(context);
2527
registerAutocomplete(context);
2628
registerHover();
27-
28-
extension.logger.client.info("Activating LiquidJava extension...");
29-
3029
await applyItalicOverlay();
3130
await startExtension(context);
3231
}

client/src/services/autocomplete.ts

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,76 @@ import { extension } from "../state";
33
import type { Variable, ContextHistory, Ghost, Alias } from "../types/context";
44
import { getSimpleName } from "../utils/utils";
55
import { getVariablesInScope } from "./context";
6-
import { LIQUIDJAVA_ANNOTATION_START } from "../utils/constants";
6+
import { LIQUIDJAVA_ANNOTATION_START, LJAnnotation } from "../utils/constants";
7+
8+
type CompletionItemOptions = {
9+
name: string;
10+
kind: vscode.CompletionItemKind;
11+
description?: string;
12+
labelDetail?: string;
13+
detail: string;
14+
documentationBlocks?: string[];
15+
codeBlocks?: string[];
16+
insertText?: string;
17+
triggerParameterHints?: boolean;
18+
}
19+
type CompletionItemKind = "vars" | "ghosts" | "aliases" | "keywords" | "types" | "decls" | "packages";
720

821
/**
922
* Registers a completion provider for LiquidJava annotations, providing context-aware suggestions based on the current context history
1023
*/
1124
export function registerAutocomplete(context: vscode.ExtensionContext) {
1225
context.subscriptions.push(
1326
vscode.languages.registerCompletionItemProvider("java", {
14-
provideCompletionItems(document, position) {
15-
if (!isInsideLiquidJavaAnnotationString(document, position) || !extension.contextHistory) return null;
27+
provideCompletionItems(document, position, _token, completionContext) {
28+
const annotation = getActiveLiquidJavaAnnotation(document, position);
29+
if (!annotation || !extension.contextHistory) return null;
30+
31+
const isDotTrigger = completionContext.triggerKind === vscode.CompletionTriggerKind.TriggerCharacter && completionContext.triggerCharacter === ".";
32+
const receiver = isDotTrigger ? getReceiverBeforeDot(document, position) : null;
1633
const file = document.uri.toString().replace("file://", "");
1734
const nextChar = document.getText(new vscode.Range(position, position.translate(0, 1)));
18-
return getContextCompletionItems(extension.contextHistory, file, nextChar);
35+
const items = getContextCompletionItems(extension.contextHistory, file, annotation, nextChar, isDotTrigger, receiver);
36+
const uniqueItems = new Map<string, vscode.CompletionItem>();
37+
items.forEach(item => {
38+
const label = typeof item.label === "string" ? item.label : item.label.label;
39+
if (!uniqueItems.has(label)) uniqueItems.set(label, item);
40+
});
41+
return Array.from(uniqueItems.values());
1942
},
20-
})
43+
}, '.', '"')
2144
);
2245
}
2346

24-
function getContextCompletionItems(context: ContextHistory, file: string, nextChar: string): vscode.CompletionItem[] {
47+
function getContextCompletionItems(context: ContextHistory, file: string, annotation: LJAnnotation, nextChar: string, isDotTrigger: boolean, receiver: string | null): vscode.CompletionItem[] {
48+
const triggerParameterHints = nextChar !== "(";
49+
if (isDotTrigger) {
50+
if (receiver === "this" || receiver === "old(this)") {
51+
return getGhostCompletionItems(context.ghosts[file] || [], triggerParameterHints);
52+
}
53+
return [];
54+
}
2555
const variablesInScope = getVariablesInScope(file, extension.selection);
2656
const inScope = variablesInScope !== null;
27-
const triggerParameterHints = nextChar !== "(";
28-
const variableItems = getVariableCompletionItems([...(variablesInScope || []), ...context.globalVars]); // not including instance vars
29-
const ghostItems = getGhostCompletionItems(context.ghosts, triggerParameterHints);
30-
const aliasItems = getAliasCompletionItems(context.aliases, triggerParameterHints);
31-
const keywordItems = getKeywordsCompletionItems(triggerParameterHints, inScope);
32-
const allItems = [...variableItems, ...ghostItems, ...aliasItems, ...keywordItems];
33-
34-
// remove duplicates
35-
const uniqueItems = new Map<string, vscode.CompletionItem>();
36-
allItems.forEach(item => {
37-
const label = typeof item.label === "string" ? item.label : item.label.label;
38-
if (!uniqueItems.has(label)) uniqueItems.set(label, item);
39-
});
40-
return Array.from(uniqueItems.values());
57+
const itemsHandlers: Record<CompletionItemKind, () => vscode.CompletionItem[]> = {
58+
vars: () => getVariableCompletionItems(variablesInScope || []),
59+
ghosts: () => getGhostCompletionItems(context.ghosts[file] || [], triggerParameterHints),
60+
aliases: () => getAliasCompletionItems(context.aliases, triggerParameterHints),
61+
keywords: () => getKeywordsCompletionItems(triggerParameterHints, inScope),
62+
types: () => getTypesCompletionItems(),
63+
decls: () => getDeclsCompletionItems(),
64+
packages: () => [], // TODO
65+
}
66+
const itemsMap: Record<LJAnnotation, CompletionItemKind[]> = {
67+
Refinement: ["vars", "ghosts", "aliases", "keywords"],
68+
StateRefinement: ["vars", "ghosts", "aliases", "keywords"],
69+
Ghost: ["types"],
70+
RefinementAlias: ["types"],
71+
RefinementPredicate: ["types", "decls"],
72+
StateSet: [],
73+
ExternalRefinementsFor: ["packages"]
74+
}
75+
return itemsMap[annotation].map(key => itemsHandlers[key]()).flat();
4176
}
4277

4378
function getVariableCompletionItems(variables: Variable[]): vscode.CompletionItem[] {
@@ -105,16 +140,21 @@ function getKeywordsCompletionItems(triggerParameterHints: boolean, inScope: boo
105140
detail: "keyword",
106141
documentationBlocks: ["Keyword referring to the **current instance**"],
107142
});
108-
const oldItem = createCompletionItem({
109-
name: "old",
143+
const oldItem = getOldKeywordCompletionItem(triggerParameterHints);
144+
const trueItem = createCompletionItem({
145+
name: "true",
110146
kind: vscode.CompletionItemKind.Keyword,
111147
description: "",
112148
detail: "keyword",
113-
documentationBlocks: ["Keyword referring to the **previous state of the current instance**"],
114-
insertText: triggerParameterHints ? "old($1)" : "old",
115-
triggerParameterHints,
116149
});
117-
const items: vscode.CompletionItem[] = [thisItem, oldItem];
150+
151+
const falseItem = createCompletionItem({
152+
name: "false",
153+
kind: vscode.CompletionItemKind.Keyword,
154+
description: "",
155+
detail: "keyword",
156+
});
157+
const items: vscode.CompletionItem[] = [thisItem, oldItem, trueItem, falseItem];
118158
if (!inScope) {
119159
const returnItem = createCompletionItem({
120160
name: "return",
@@ -128,16 +168,36 @@ function getKeywordsCompletionItems(triggerParameterHints: boolean, inScope: boo
128168
return items;
129169
}
130170

131-
type CompletionItemOptions = {
132-
name: string;
133-
kind: vscode.CompletionItemKind;
134-
description?: string;
135-
labelDetail?: string;
136-
detail: string;
137-
documentationBlocks?: string[];
138-
codeBlocks?: string[];
139-
insertText?: string;
140-
triggerParameterHints?: boolean;
171+
function getOldKeywordCompletionItem(triggerParameterHints: boolean): vscode.CompletionItem {
172+
return createCompletionItem({
173+
name: "old",
174+
kind: vscode.CompletionItemKind.Keyword,
175+
description: "",
176+
detail: "keyword",
177+
documentationBlocks: ["Keyword referring to the **previous state of the current instance**"],
178+
insertText: triggerParameterHints ? "old($1)" : "old",
179+
triggerParameterHints,
180+
});
181+
}
182+
183+
function getTypesCompletionItems(): vscode.CompletionItem[] {
184+
const types = ["int", "double", "float", "boolean"];
185+
return types.map(type => createCompletionItem({
186+
name: type,
187+
kind: vscode.CompletionItemKind.Keyword,
188+
description: "",
189+
detail: "keyword",
190+
}));
191+
}
192+
193+
function getDeclsCompletionItems(): vscode.CompletionItem[] {
194+
const decls = ["ghost", "type"]
195+
return decls.map(decl => createCompletionItem({
196+
name: decl,
197+
kind: vscode.CompletionItemKind.Keyword,
198+
description: "",
199+
detail: "keyword",
200+
}));
141201
}
142202

143203
function createCompletionItem({ name, kind, labelDetail, description, detail, documentationBlocks, codeBlocks, insertText, triggerParameterHints }: CompletionItemOptions): vscode.CompletionItem {
@@ -155,15 +215,17 @@ function createCompletionItem({ name, kind, labelDetail, description, detail, do
155215
return item;
156216
}
157217

158-
function isInsideLiquidJavaAnnotationString(document: vscode.TextDocument, position: vscode.Position): boolean {
218+
function getActiveLiquidJavaAnnotation(document: vscode.TextDocument, position: vscode.Position): LJAnnotation | null {
159219
const textUntilCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
160220
LIQUIDJAVA_ANNOTATION_START.lastIndex = 0;
161221
let match: RegExpExecArray | null = null;
162222
let lastAnnotationStart = -1;
223+
let lastAnnotationName: LJAnnotation | null = null;
163224
while ((match = LIQUIDJAVA_ANNOTATION_START.exec(textUntilCursor)) !== null) {
164225
lastAnnotationStart = match.index;
226+
lastAnnotationName = match[2] as LJAnnotation || null;
165227
}
166-
if (lastAnnotationStart === -1) return false;
228+
if (lastAnnotationStart === -1 || !lastAnnotationName) return null;
167229

168230
const fromLastAnnotation = textUntilCursor.slice(lastAnnotationStart);
169231
let parenthesisDepth = 0;
@@ -179,5 +241,14 @@ function isInsideLiquidJavaAnnotationString(document: vscode.TextDocument, posit
179241
if (char === "(") parenthesisDepth++;
180242
if (char === ")") parenthesisDepth--;
181243
}
182-
return parenthesisDepth > 0;
244+
return parenthesisDepth > 0 ? lastAnnotationName : null;
245+
}
246+
247+
function getReceiverBeforeDot(document: vscode.TextDocument, position: vscode.Position): string | null {
248+
const prefix = document.lineAt(position.line).text.slice(0, position.character);
249+
const match = prefix.match(/((?:old\s*\(\s*this\s*\))|(?:[A-Za-z_]\w*))\.\w*$/);
250+
if (!match) return null;
251+
const receiver = match[1].trim();
252+
if (/^old\s*\(\s*this\s*\)$/.test(receiver)) return "old(this)";
253+
return receiver;
183254
}

client/src/services/context.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ function isSelectionWithinScope(selection: Selection, scope: Selection): boolean
5656
function getReachableVariables(variables: Variable[], selection: Selection): Variable[] {
5757
return variables.filter((variable) => {
5858
const placement = variable.placementInCode?.position;
59-
if (!placement) return true;
60-
return placement.line < selection.startLine || (placement.line === selection.startLine && placement.column <= selection.startColumn);
59+
const startPosition = variable.annPosition || placement;
60+
if (!startPosition || variable.isParameter) return true; // if is parameter we need to access it even if it's declared after the selection (for method and parameter refinements)
61+
62+
// variable was declared before the cursor line or its in the same line but before the cursor column
63+
return startPosition.line < selection.startLine || startPosition.line === selection.startLine && startPosition.column <= selection.startColumn;
6164
});
6265
}

client/src/types/context.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PlacementInCode } from "./diagnostics";
1+
import { PlacementInCode, SourcePosition } from "./diagnostics";
22

33
// Type definitions used for LiquidJava context information
44

@@ -8,6 +8,8 @@ export type Variable = {
88
refinement: string;
99
mainRefinement: string;
1010
placementInCode: PlacementInCode | null;
11+
isParameter: boolean;
12+
annPosition: SourcePosition | null;
1113
}
1214

1315
export type Ghost = {
@@ -27,9 +29,9 @@ export type Alias = {
2729

2830
export type ContextHistory = {
2931
vars: Record<string, Record<string, Variable[]>>; // file -> (scope -> variables in scope)
32+
ghosts: Record<string, Ghost[]>; // file -> ghosts in file
3033
instanceVars: Variable[];
3134
globalVars: Variable[];
32-
ghosts: Ghost[];
3335
aliases: Alias[];
3436
}
3537

client/src/types/diagnostics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ export type ErrorPosition = {
99
colEnd: number;
1010
}
1111

12-
export type Position = {
12+
export type SourcePosition = {
1313
file: string;
1414
line: number;
1515
column: number;
1616
}
1717

1818
export type PlacementInCode = {
1919
text: string;
20-
position: Position;
20+
position: SourcePosition;
2121
}
2222

2323
export type TranslationTable = Record<string, PlacementInCode>;

client/src/utils/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export const LIQUIDJAVA_ANNOTATIONS = [
2727
"StateRefinement",
2828
"ExternalRefinementsFor",
2929
]
30-
export const LIQUIDJAVA_ANNOTATION_START = new RegExp(`@(liquidjava\\.specification\\.)?(${LIQUIDJAVA_ANNOTATIONS.join("|")})\\s*\\(`, "g");
30+
export const LIQUIDJAVA_ANNOTATION_START = new RegExp(`@(liquidjava\\.specification\\.)?(${LIQUIDJAVA_ANNOTATIONS.join("|")})\\s*\\(`, "g");
31+
export type LJAnnotation = "Refinement" | "RefinementAlias" | "RefinementPredicate" | "StateSet" | "Ghost" | "StateRefinement" | "ExternalRefinementsFor";

server/pom.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,6 @@
169169
</dependencyManagement>
170170

171171
<dependencies>
172-
<!-- <dependency>
173-
<groupId>liquidjava</groupId>
174-
<artifactId>verifier</artifactId>
175-
<version>0.0.13</version>
176-
<scope>system</scope>
177-
<systemPath>${project.basedir}/libs/liquidjava-verifier.jar</systemPath>
178-
</dependency> -->
179172
<dependency>
180173
<groupId>io.github.liquid-java</groupId>
181174
<artifactId>liquidjava-verifier</artifactId>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public record ContextHistoryDTO(
1212
Map<String, Map<String, List<VariableDTO>>> vars,
1313
List<VariableDTO> instanceVars,
1414
List<VariableDTO> globalVars,
15-
List<GhostDTO> ghosts,
15+
Map<String, List<GhostDTO>> ghosts,
1616
List<AliasDTO> aliases
1717
) {
1818
public static String stringifyType(CtTypeReference<?> typeReference) {
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package dtos.context;
22

33
import dtos.diagnostics.PlacementInCodeDTO;
4+
import dtos.diagnostics.SourcePositionDTO;
45
import liquidjava.processor.context.RefinedVariable;
5-
import spoon.reflect.reference.CtTypeReference;
66

77
/**
88
* DTO for serializing RefinedVariable instances to JSON.
@@ -12,15 +12,19 @@ public record VariableDTO(
1212
String type,
1313
String refinement,
1414
String mainRefinement,
15-
PlacementInCodeDTO placementInCode
15+
PlacementInCodeDTO placementInCode,
16+
boolean isParameter,
17+
SourcePositionDTO annPosition
1618
) {
1719
public static VariableDTO from(RefinedVariable refinedVariable) {
1820
return new VariableDTO(
1921
refinedVariable.getName(),
2022
ContextHistoryDTO.stringifyType(refinedVariable.getType()),
2123
refinedVariable.getRefinement().toString(),
2224
refinedVariable.getMainRefinement().toString(),
23-
PlacementInCodeDTO.from(refinedVariable.getPlacementInCode())
25+
PlacementInCodeDTO.from(refinedVariable.getPlacementInCode()),
26+
refinedVariable.isParameter(),
27+
SourcePositionDTO.from(refinedVariable.getAnnPosition())
2428
);
2529
}
2630
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
/**
66
* DTO for serializing PlacementInCode instances to JSON
77
*/
8-
public record PlacementInCodeDTO(String text, PositionDTO position) {
8+
public record PlacementInCodeDTO(String text, SourcePositionDTO position) {
99

1010
public static PlacementInCodeDTO from(PlacementInCode placement) {
1111
if (placement == null)
1212
return null;
13-
return new PlacementInCodeDTO(placement.getText(), PositionDTO.from(placement.getPosition()));
13+
return new PlacementInCodeDTO(placement.getText(), SourcePositionDTO.from(placement.getPosition()));
1414
}
1515
}

0 commit comments

Comments
 (0)