Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.

Commit 21b6a94

Browse files
authored
Merge pull request #168 from DataFlowAnalysis/single-constraint-input
Move to single constraint input
2 parents e3b212d + 9949c02 commit 21b6a94

File tree

11 files changed

+266
-259
lines changed

11 files changed

+266
-259
lines changed

src/features/constraintMenu/AutoCompletion.ts

Lines changed: 117 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ export interface RequiredCompletionParts {
88

99
export interface ValidationError {
1010
message: string;
11+
line: number;
1112
startColumn: number;
1213
endColumn: number;
1314
}
1415

16+
interface Token {
17+
text: string;
18+
line: number;
19+
column: number;
20+
}
21+
1522
export type WordCompletion = RequiredCompletionParts & Partial<monaco.languages.CompletionItem>;
1623

1724
export interface AbstractWord {
@@ -89,133 +96,193 @@ export class NegatableWord implements AbstractWord {
8996
}
9097

9198
export class AutoCompleteTree {
92-
private content: string[];
93-
/** value matches the start column of the value at the same index in content */
94-
private startColumns: number[];
95-
private length: number;
96-
97-
constructor(private roots: AutoCompleteNode[]) {
98-
this.content = [];
99-
this.startColumns = [];
100-
this.length = 0;
101-
}
99+
constructor(private roots: AutoCompleteNode[]) {}
102100

103-
/**
104-
* Sets the content of the tree for the next analyzing cycle
105-
*/
106-
public setContent(line: string) {
107-
if (line.length == 0) {
108-
this.content = [];
109-
this.length = 0;
110-
return;
111-
}
112-
this.content = line.split(" ");
113-
this.startColumns = this.content.map(() => 0);
114-
for (let i = 1; i < this.content.length; i++) {
115-
this.startColumns[i] = this.startColumns[i - 1] + this.content[i - 1].length + 1;
116-
}
117-
this.length = line.length;
101+
private tokenize(text: string[]): Token[] {
102+
if (!text || text.length == 0) {
103+
return [];
104+
}
105+
106+
const tokens: Token[] = [];
107+
for (const [lineNumber, line] of text.entries()) {
108+
const lineTokens = line.split(/\s+/).filter((t) => t.length > 0);
109+
let column = 0;
110+
for (const token of lineTokens) {
111+
column = line.indexOf(token, column);
112+
tokens.push({
113+
text: token,
114+
line: lineNumber + 1,
115+
column: column + 1,
116+
});
117+
}
118+
}
119+
120+
return tokens;
118121
}
119122

120123
/**
121124
* Checks the set content for errors
122125
* @returns An array of errors. An empty array means that the content is valid
123126
*/
124-
public verify(): ValidationError[] {
125-
return this.verifyNode(this.roots, 0, false);
127+
public verify(lines: string[]): ValidationError[] {
128+
const tokens = this.tokenize(lines);
129+
return this.verifyNode(this.roots, tokens, 0, false, true);
126130
}
127131

128-
private verifyNode(nodes: AutoCompleteNode[], index: number, comesFromFinal: boolean): ValidationError[] {
129-
if (index >= this.content.length) {
132+
private verifyNode(
133+
nodes: AutoCompleteNode[],
134+
tokens: Token[],
135+
index: number,
136+
comesFromFinal: boolean,
137+
skipStartCheck = false,
138+
): ValidationError[] {
139+
if (index >= tokens.length) {
130140
if (nodes.length == 0 || comesFromFinal) {
131141
return [];
132142
} else {
133-
return [{ message: "Unexpected end of line", startColumn: this.length - 1, endColumn: this.length }];
143+
return [
144+
{
145+
message: "Unexpected end of line",
146+
line: tokens[index - 1].line,
147+
startColumn: tokens[index - 1].column + tokens[index - 1].text.length - 1,
148+
endColumn: tokens[index - 1].column + tokens[index - 1].text.length,
149+
},
150+
];
151+
}
152+
}
153+
if (!skipStartCheck && tokens[index].column == 1) {
154+
const matchesAnyRoot = this.roots.some((r) => r.word.verifyWord(tokens[index].text).length === 0);
155+
if (matchesAnyRoot) {
156+
return this.verifyNode(this.roots, tokens, index, false, true);
134157
}
135158
}
136159

137160
const foundErrors: ValidationError[] = [];
138161
let childErrors: ValidationError[] = [];
139162
for (const n of nodes) {
140-
const v = n.word.verifyWord(this.content[index]);
163+
const v = n.word.verifyWord(tokens[index].text);
141164
if (v.length > 0) {
142165
foundErrors.push({
143166
message: v[0],
144-
startColumn: this.startColumns[index],
145-
endColumn: this.startColumns[index] + this.content[index].length,
167+
startColumn: tokens[index].column,
168+
endColumn: tokens[index].column + tokens[index].text.length,
169+
line: tokens[index].line,
146170
});
147171
continue;
148172
}
149173

150-
const childResult = this.verifyNode(n.children, index + 1, n.canBeFinal || false);
174+
const childResult = this.verifyNode(n.children, tokens, index + 1, n.canBeFinal || false);
151175
if (childResult.length == 0) {
152176
return [];
153177
} else {
154178
childErrors = childErrors.concat(childResult);
155179
}
156180
}
157181
if (childErrors.length > 0) {
158-
return childErrors;
182+
return deduplicateErrors(childErrors);
159183
}
160-
return foundErrors;
184+
return deduplicateErrors(foundErrors);
161185
}
162186

163187
/**
164188
* Calculates the completion options for the current content
165189
*/
166-
public getCompletion(): monaco.languages.CompletionItem[] {
190+
public getCompletion(lines: string[]): monaco.languages.CompletionItem[] {
191+
const tokens = this.tokenize(lines);
192+
const endsWithWhitespace =
193+
(lines.length > 0 && lines[lines.length - 1].charAt(lines[lines.length - 1].length - 1).match(/\s/)) ||
194+
lines[lines.length - 1].length == 0;
195+
if (endsWithWhitespace) {
196+
tokens.push({
197+
text: "",
198+
line: lines.length,
199+
column: lines[lines.length - 1].length + 1,
200+
});
201+
}
167202
let result: WordCompletion[] = [];
168-
if (this.content.length == 0) {
203+
if (tokens.length == 0) {
169204
for (const r of this.roots) {
170205
result = result.concat(r.word.completionOptions(""));
171206
}
172207
} else {
173-
result = this.completeNode(this.roots, 0);
208+
result = this.completeNode(this.roots, tokens, 0);
174209
}
175-
return this.transformResults(result);
210+
return this.transformResults(result, tokens);
176211
}
177212

178-
private completeNode(nodes: AutoCompleteNode[], index: number): WordCompletion[] {
213+
private completeNode(
214+
nodes: AutoCompleteNode[],
215+
tokens: Token[],
216+
index: number,
217+
skipStartCheck = false,
218+
): WordCompletion[] {
219+
// check for new start
220+
221+
if (!skipStartCheck && tokens[index].column == 1) {
222+
const matchesAnyRoot = this.roots.some((n) => n.word.verifyWord(tokens[index].text).length === 0);
223+
if (matchesAnyRoot) {
224+
return this.completeNode(this.roots, tokens, index, true);
225+
}
226+
}
227+
179228
let result: WordCompletion[] = [];
180-
if (index == this.content.length - 1) {
229+
if (index == tokens.length - 1) {
181230
for (const node of nodes) {
182-
result = result.concat(node.word.completionOptions(this.content[index]));
231+
result = result.concat(node.word.completionOptions(tokens[index].text));
183232
}
184233
return result;
185234
}
186235
for (const n of nodes) {
187-
if (!n.word.verifyWord(this.content[index])) {
236+
if (!n.word.verifyWord(tokens[index].text)) {
188237
continue;
189238
}
190-
result = result.concat(this.completeNode(n.children, index + 1));
239+
result = result.concat(this.completeNode(n.children, tokens, index + 1));
191240
}
192241
return result;
193242
}
194243

195-
private transformResults(comp: WordCompletion[]): monaco.languages.CompletionItem[] {
244+
private transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] {
196245
const result: monaco.languages.CompletionItem[] = [];
197246
const filtered = comp.filter(
198247
(c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx,
199248
);
200249
for (const c of filtered) {
201-
const r = this.transformResult(c);
250+
const r = this.transformResult(c, tokens);
202251
result.push(r);
203252
}
204253
return result;
205254
}
206255

207-
private transformResult(comp: WordCompletion): monaco.languages.CompletionItem {
208-
const wordStart = this.content.length == 0 ? 1 : this.length - this.content[this.content.length - 1].length + 1;
256+
private transformResult(comp: WordCompletion, tokens: Token[]): monaco.languages.CompletionItem {
257+
const wordStart = tokens.length == 0 ? 1 : tokens[tokens.length - 1].column;
258+
const lineNumber = tokens.length == 0 ? 1 : tokens[tokens.length - 1].line;
209259
return {
210260
insertText: comp.insertText,
211261
kind: comp.kind,
212262
label: comp.label ?? comp.insertText,
213263
insertTextRules: comp.insertTextRules,
214-
range: new monaco.Range(1, wordStart + (comp.startOffset ?? 0), 1, this.length + 1),
264+
range: new monaco.Range(
265+
lineNumber,
266+
wordStart + (comp.startOffset ?? 0),
267+
lineNumber,
268+
wordStart + (comp.startOffset ?? 0) + comp.insertText.length,
269+
),
215270
};
216271
}
217272
}
218273

274+
function deduplicateErrors(errors: ValidationError[]): ValidationError[] {
275+
const seen = new Set<string>();
276+
return errors.filter((error) => {
277+
const key = `${error.line}-${error.startColumn}-${error.endColumn}-${error.message}`;
278+
if (seen.has(key)) {
279+
return false;
280+
}
281+
seen.add(key);
282+
return true;
283+
});
284+
}
285+
219286
export interface AutoCompleteNode {
220287
word: AbstractWord;
221288
children: AutoCompleteNode[];

0 commit comments

Comments
 (0)