Skip to content

Commit fe45f34

Browse files
authored
feat: completed completion (#15)
1 parent cd4d08f commit fe45f34

File tree

2 files changed

+309
-22
lines changed

2 files changed

+309
-22
lines changed

src/sdl/sdlComplete.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import {
2+
CompletionContext,
3+
type CompletionResult,
4+
} from "@codemirror/autocomplete";
5+
import type { SyntaxNode, Tree } from "@lezer/common";
6+
import { syntaxTree } from "@codemirror/language";
7+
8+
const contextualCompletionStringsMap: Record<string, string[]> = {
9+
unsigned: ["int"],
10+
reserved: [
11+
"const",
12+
"aligned",
13+
"int",
14+
"unsigned",
15+
"float",
16+
"bit",
17+
"utf16string",
18+
"utf8string",
19+
"utf8list",
20+
"utfstring",
21+
"base64string",
22+
],
23+
legacy: [
24+
"const",
25+
"aligned",
26+
"int",
27+
"unsigned",
28+
"float",
29+
"bit",
30+
"utf16string",
31+
"utf8string",
32+
"utf8list",
33+
"utfstring",
34+
"base64string",
35+
],
36+
const: [
37+
"aligned",
38+
"int",
39+
"unsigned",
40+
"float",
41+
"bit",
42+
"utf16string",
43+
"utf8string",
44+
"utf8list",
45+
"utfstring",
46+
"base64string",
47+
],
48+
aligned: [
49+
"int",
50+
"unsigned",
51+
"float",
52+
"bit",
53+
"utf16string",
54+
"utf8string",
55+
"utf8list",
56+
"utfstring",
57+
"base64string",
58+
],
59+
};
60+
61+
const globalScopeCompletionStrings: string[] = [
62+
"computed",
63+
"map",
64+
"aligned",
65+
"expandable",
66+
"abstract",
67+
"class",
68+
];
69+
70+
const blockScopeCompletionStrings: string[] = [
71+
"if",
72+
"switch",
73+
"do",
74+
"for",
75+
"while",
76+
"reserved",
77+
"legacy",
78+
"const",
79+
"aligned",
80+
"int",
81+
"unsigned",
82+
"float",
83+
"bit",
84+
"utf16string",
85+
"utf8string",
86+
"utf8list",
87+
"utfstring",
88+
"base64string",
89+
];
90+
91+
const caseClauseCompletionStrings: string[] = [
92+
"break",
93+
...blockScopeCompletionStrings,
94+
];
95+
96+
const switchScopeCompletionStrings: string[] = [
97+
"case",
98+
"default",
99+
];
100+
101+
function isGlobalScopeCompletion(lastNode: SyntaxNode): boolean {
102+
// see if the last node is a specification
103+
if (lastNode.type.name === "Specification") {
104+
return true;
105+
}
106+
107+
// see if the parent of the last node is a specification
108+
const parentNode = lastNode.parent;
109+
110+
return parentNode?.type.name === "Specification";
111+
}
112+
113+
function previousTokenIsOneOf(node: SyntaxNode, tokenNames: string[]): boolean {
114+
// loop backwards through the siblings of the current node to find the previous token that matches one of the specified token names
115+
let currentNode: SyntaxNode | null = node;
116+
117+
while (currentNode) {
118+
// stop looking if we hit a non-matching whitespace node
119+
if (currentNode.type.name === "Whitespace") {
120+
currentNode = currentNode.prevSibling;
121+
continue;
122+
}
123+
console.error("currentNode", currentNode.name);
124+
return (tokenNames.includes(currentNode.type.name));
125+
}
126+
127+
return false;
128+
}
129+
130+
const statements = [
131+
"CompoundStatement",
132+
"IfStatement",
133+
"SwitchStatement",
134+
"ForStatement",
135+
"DoStatement",
136+
"WhileStatement",
137+
"ExpressionStatement",
138+
"ElementaryTypeDefinition",
139+
"MapDefinition",
140+
"ClassDefinition",
141+
"StringDefinition",
142+
"ArrayDefinition",
143+
"ComputedElementaryTypeDefinition",
144+
"ComputedArrayDefinition",
145+
"Comment",
146+
];
147+
148+
function isBlockScopeCompletion(lastNode: SyntaxNode): boolean {
149+
// see if the parent of the last node is a block scoped node
150+
const parentNode = lastNode.parent;
151+
152+
if (!parentNode) {
153+
return false;
154+
}
155+
156+
const name = parentNode.type.name;
157+
158+
if (
159+
(name === "ClassDeclaration") &&
160+
(previousTokenIsOneOf(lastNode, ["OpenBrace", ...statements]))
161+
) {
162+
return true;
163+
}
164+
if (
165+
(name === "CompoundStatement") &&
166+
(previousTokenIsOneOf(lastNode, ["OpenBrace", ...statements]))
167+
) {
168+
return true;
169+
}
170+
if (
171+
(name === "IfStatement") &&
172+
(previousTokenIsOneOf(lastNode, ["CloseParenthesis", "else"]))
173+
) {
174+
return true;
175+
}
176+
if (
177+
(name === "DefaultClause") && (previousTokenIsOneOf(lastNode, ["Colon"]))
178+
) {
179+
return true;
180+
}
181+
182+
return false;
183+
}
184+
185+
function isSwitchScopeCompletion(lastNode: SyntaxNode): boolean {
186+
// see if the parent of the last node is a switch statement
187+
const parentNode = lastNode.parent;
188+
189+
return (parentNode?.type.name === "SwitchStatement") &&
190+
previousTokenIsOneOf(lastNode, ["OpenBrace"]);
191+
}
192+
193+
function isCaseClauseCompletion(lastNode: SyntaxNode): boolean {
194+
// see if the parent of the last node is a case clause
195+
const parentNode = lastNode.parent;
196+
console.error("lastNode ", lastNode.name, "parentNode", parentNode?.name);
197+
return ((parentNode?.type.name === "SwitchStatement") ||
198+
(parentNode?.type.name === "CaseClause")) &&
199+
previousTokenIsOneOf(lastNode, [
200+
"Colon",
201+
"OpenBrace",
202+
"CaseClause",
203+
...statements,
204+
]);
205+
}
206+
207+
function getContextualCompletionOptions(
208+
tree: Tree,
209+
lastNode: SyntaxNode,
210+
): string[] | null {
211+
// see if we are completing after whitespace
212+
if (lastNode.type.name === "Whitespace") {
213+
// look for the node before the current whitespace
214+
const secondLastNode = tree.resolveInner(lastNode.from - 1, -1);
215+
216+
// if there is a previous node, we can provide completions based on that
217+
if (secondLastNode) {
218+
return contextualCompletionStringsMap[secondLastNode.type.name];
219+
}
220+
}
221+
222+
// see if we are completing partway through an identifier (i.e. a word not yet recognized as a keyword or type name)
223+
if (lastNode.type.name === "Identifier") {
224+
// look for the previous node before the partly typed identifier
225+
const secondLastNode = tree.resolveInner(lastNode.from - 1, -1);
226+
227+
// if it is whitespace, we can provide completions based on the previous node before the whitespace
228+
if (secondLastNode.type.name === "Whitespace") {
229+
const thirdLastNode = tree.resolveInner(secondLastNode.from - 1, -1);
230+
231+
if (thirdLastNode.type.name !== "Whitespace") {
232+
return contextualCompletionStringsMap[thirdLastNode.type.name];
233+
}
234+
}
235+
}
236+
237+
return null;
238+
}
239+
240+
function getCompletionResult(
241+
completionOptions: string[],
242+
from: number,
243+
): CompletionResult {
244+
return {
245+
from,
246+
options: completionOptions.map((label) => ({ label })),
247+
validFor: /^\w*$/,
248+
};
249+
}
250+
251+
function sdlComplete(context: CompletionContext): CompletionResult | null {
252+
const tree = syntaxTree(context.state);
253+
const lastNode = tree.resolveInner(context.pos, -1);
254+
const lastText = context.state.sliceDoc(lastNode.from, context.pos);
255+
const lastTag = /^\w*$/.exec(lastText);
256+
257+
if (isGlobalScopeCompletion(lastNode)) {
258+
// only provide completions at the global scope if completion was explicitly requested
259+
if (!context.explicit) {
260+
return null;
261+
}
262+
263+
return getCompletionResult(
264+
globalScopeCompletionStrings,
265+
lastTag ? lastNode.from + lastTag.index : context.pos,
266+
);
267+
}
268+
269+
if (isBlockScopeCompletion(lastNode)) {
270+
// only provide completions at the block scope if completion was explicitly requested
271+
if (!context.explicit) {
272+
return null;
273+
}
274+
275+
return getCompletionResult(
276+
blockScopeCompletionStrings,
277+
lastTag ? lastNode.from + lastTag.index : context.pos,
278+
);
279+
}
280+
281+
if (isCaseClauseCompletion(lastNode)) {
282+
return getCompletionResult(
283+
caseClauseCompletionStrings,
284+
lastTag ? lastNode.from + lastTag.index : context.pos,
285+
);
286+
}
287+
288+
if (isSwitchScopeCompletion(lastNode)) {
289+
return getCompletionResult(
290+
switchScopeCompletionStrings,
291+
lastTag ? lastNode.from + lastTag.index : context.pos,
292+
);
293+
}
294+
295+
const completions = getContextualCompletionOptions(tree, lastNode);
296+
297+
if (completions) {
298+
return getCompletionResult(
299+
completions,
300+
lastTag ? lastNode.from + lastTag.index : context.pos,
301+
);
302+
}
303+
304+
return null;
305+
}
306+
307+
export { sdlComplete };

src/sdl/sdlLanguage.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { LRParser as LezerParser } from "@lezer/lr";
22
import { type Extension } from "@codemirror/state";
33
import { styleTags, tags as t } from "@lezer/highlight";
4-
import { CompletionContext } from "@codemirror/autocomplete";
54
import { createLenientSdlParser } from "@flowscripter/mpeg-sdl-parser";
65
import {
76
foldNodeProp,
87
LanguageSupport,
98
LRLanguage,
10-
syntaxTree,
119
} from "@codemirror/language";
10+
import { sdlComplete } from "./sdlComplete";
1211

1312
export function createParser(): LezerParser {
1413
const parser = createLenientSdlParser();
@@ -115,27 +114,8 @@ export const sdlLanguage = LRLanguage.define({
115114
},
116115
});
117116

118-
function completeSdl(context: CompletionContext) {
119-
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
120-
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
121-
const tagBefore = /@\w*$/.exec(textBefore);
122-
123-
if (!tagBefore && !context.explicit) {
124-
return null;
125-
}
126-
127-
return {
128-
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
129-
options: [
130-
{ label: "class", type: "keyword" },
131-
{ label: "extends", type: "keyword" },
132-
],
133-
validFor: /^\w*$/,
134-
};
135-
}
136-
137117
export const sdlCompletion = sdlLanguage.data.of({
138-
autocomplete: completeSdl,
118+
autocomplete: sdlComplete,
139119
});
140120

141121
export function sdl(): Extension {

0 commit comments

Comments
 (0)