1+ import * as vscode from "vscode" ;
2+ import { extension } from "../state" ;
3+ import type { Variable , ContextHistory , Ghost , Alias } from "../types/context" ;
4+ import { getSimpleName } from "../utils/utils" ;
5+ import { getVariablesInScope } from "./context" ;
6+ import { LIQUIDJAVA_ANNOTATION_START } from "../utils/constants" ;
7+
8+ /**
9+ * Registers a completion provider for LiquidJava annotations, providing context-aware suggestions based on the current context history
10+ */
11+ export function registerAutocomplete ( context : vscode . ExtensionContext ) {
12+ context . subscriptions . push (
13+ vscode . languages . registerCompletionItemProvider ( "java" , {
14+ provideCompletionItems ( document , position ) {
15+ if ( ! isInsideLiquidJavaAnnotationString ( document , position ) || ! extension . contextHistory ) return null ;
16+ const file = document . uri . toString ( ) . replace ( "file://" , "" ) ;
17+ const nextChar = document . getText ( new vscode . Range ( position , position . translate ( 0 , 1 ) ) ) ;
18+ return getContextCompletionItems ( extension . contextHistory , file , nextChar ) ;
19+ } ,
20+ } )
21+ ) ;
22+ }
23+
24+ function getContextCompletionItems ( context : ContextHistory , file : string , nextChar : string ) : vscode . CompletionItem [ ] {
25+ const variablesInScope = getVariablesInScope ( file , extension . selection ) ;
26+ 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 ( ) ) ;
41+ }
42+
43+ function getVariableCompletionItems ( variables : Variable [ ] ) : vscode . CompletionItem [ ] {
44+ return variables . map ( variable => {
45+ const varSig = `${ variable . type } ${ variable . name } ` ;
46+ const codeBlocks : string [ ] = [ ] ;
47+ if ( variable . mainRefinement !== "true" ) codeBlocks . push ( `@Refinement("${ variable . mainRefinement } ")` ) ;
48+ codeBlocks . push ( varSig ) ;
49+ return createCompletionItem ( {
50+ name : variable . name ,
51+ kind : vscode . CompletionItemKind . Variable ,
52+ description : variable . type ,
53+ detail : "variable" ,
54+ codeBlocks,
55+ } ) ;
56+ } ) ;
57+ }
58+
59+ function getGhostCompletionItems ( ghosts : Ghost [ ] , triggerParameterHints : boolean ) : vscode . CompletionItem [ ] {
60+ return ghosts . map ( ghost => {
61+ const parameters = ghost . parameterTypes . map ( getSimpleName ) . join ( ", " ) ;
62+ const ghostSig = `${ ghost . returnType } ${ ghost . name } (${ parameters } )` ;
63+ const isState = / ^ s t a t e \d + \( _ \) = = \d + $ / . test ( ghost . refinement ) ;
64+ const description = isState ? "state" : "ghost" ;
65+ return createCompletionItem ( {
66+ name : ghost . name ,
67+ kind : vscode . CompletionItemKind . Function ,
68+ labelDetail : `(${ parameters } )` ,
69+ description,
70+ detail : description ,
71+ codeBlocks : [ ghostSig ] ,
72+ insertText : triggerParameterHints ? `${ ghost . name } ($1)` : ghost . name ,
73+ triggerParameterHints,
74+ } ) ;
75+ } ) ;
76+ }
77+
78+ function getAliasCompletionItems ( aliases : Alias [ ] , triggerParameterHints : boolean ) : vscode . CompletionItem [ ] {
79+ return aliases . map ( alias => {
80+ const parameters = alias . parameters
81+ . map ( ( parameter , index ) => {
82+ const type = getSimpleName ( alias . types [ index ] ) ;
83+ return `${ type } ${ parameter } ` ;
84+ } ) . join ( ", " ) ;
85+ const aliasSig = `${ alias . name } (${ parameters } ) { ${ alias . predicate } }` ;
86+ const description = "alias" ;
87+ return createCompletionItem ( {
88+ name : alias . name ,
89+ kind : vscode . CompletionItemKind . Function ,
90+ labelDetail : `(${ parameters } ){ ${ alias . predicate } }` ,
91+ description,
92+ detail : description ,
93+ codeBlocks : [ aliasSig ] ,
94+ insertText : triggerParameterHints ? `${ alias . name } ($1)` : alias . name ,
95+ triggerParameterHints,
96+ } ) ;
97+ } ) ;
98+ }
99+
100+ function getKeywordsCompletionItems ( triggerParameterHints : boolean , inScope : boolean ) : vscode . CompletionItem [ ] {
101+ const thisItem = createCompletionItem ( {
102+ name : "this" ,
103+ kind : vscode . CompletionItemKind . Keyword ,
104+ description : "" ,
105+ detail : "keyword" ,
106+ documentationBlocks : [ "Keyword referring to the **current instance**" ] ,
107+ } ) ;
108+ const oldItem = createCompletionItem ( {
109+ name : "old" ,
110+ kind : vscode . CompletionItemKind . Keyword ,
111+ description : "" ,
112+ detail : "keyword" ,
113+ documentationBlocks : [ "Keyword referring to the **previous state of the current instance**" ] ,
114+ insertText : triggerParameterHints ? "old($1)" : "old" ,
115+ triggerParameterHints,
116+ } ) ;
117+ const items : vscode . CompletionItem [ ] = [ thisItem , oldItem ] ;
118+ if ( ! inScope ) {
119+ const returnItem = createCompletionItem ( {
120+ name : "return" ,
121+ kind : vscode . CompletionItemKind . Keyword ,
122+ description : "" ,
123+ detail : "keyword" ,
124+ documentationBlocks : [ "Keyword referring to the **method return value**" ] ,
125+ } ) ;
126+ items . push ( returnItem ) ;
127+ }
128+ return items ;
129+ }
130+
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 ;
141+ }
142+
143+ function createCompletionItem ( { name, kind, labelDetail, description, detail, documentationBlocks, codeBlocks, insertText, triggerParameterHints } : CompletionItemOptions ) : vscode . CompletionItem {
144+ const item = new vscode . CompletionItem ( name , kind ) ;
145+ item . label = { label : name , detail : labelDetail , description } ;
146+ item . detail = detail ;
147+ if ( insertText ) item . insertText = new vscode . SnippetString ( insertText ) ;
148+ if ( triggerParameterHints ) item . command = { command : "editor.action.triggerParameterHints" , title : "Trigger Parameter Hints" } ;
149+
150+ const documentation = new vscode . MarkdownString ( ) ;
151+ if ( documentationBlocks ) documentationBlocks . forEach ( block => documentation . appendMarkdown ( block ) ) ;
152+ if ( codeBlocks ) codeBlocks . forEach ( block => documentation . appendCodeblock ( block ) ) ;
153+ item . documentation = documentation ;
154+
155+ return item ;
156+ }
157+
158+ function isInsideLiquidJavaAnnotationString ( document : vscode . TextDocument , position : vscode . Position ) : boolean {
159+ const textUntilCursor = document . getText ( new vscode . Range ( new vscode . Position ( 0 , 0 ) , position ) ) ;
160+ LIQUIDJAVA_ANNOTATION_START . lastIndex = 0 ;
161+ let match : RegExpExecArray | null = null ;
162+ let lastAnnotationStart = - 1 ;
163+ while ( ( match = LIQUIDJAVA_ANNOTATION_START . exec ( textUntilCursor ) ) !== null ) {
164+ lastAnnotationStart = match . index ;
165+ }
166+ if ( lastAnnotationStart === - 1 ) return false ;
167+
168+ const fromLastAnnotation = textUntilCursor . slice ( lastAnnotationStart ) ;
169+ let parenthesisDepth = 0 ;
170+ let isInsideString = false ;
171+ for ( let i = 0 ; i < fromLastAnnotation . length ; i ++ ) {
172+ const char = fromLastAnnotation [ i ] ;
173+ const previousChar = i > 0 ? fromLastAnnotation [ i - 1 ] : "" ;
174+ if ( char === '"' && previousChar !== "\\" ) {
175+ isInsideString = ! isInsideString ;
176+ continue ;
177+ }
178+ if ( isInsideString ) continue ;
179+ if ( char === "(" ) parenthesisDepth ++ ;
180+ if ( char === ")" ) parenthesisDepth -- ;
181+ }
182+ return parenthesisDepth > 0 ;
183+ }
0 commit comments