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

Commit 8acf1ec

Browse files
committed
Merge: single constraint input and fix issues
2 parents 4dada2d + 3d311b0 commit 8acf1ec

5 files changed

Lines changed: 180 additions & 122 deletions

File tree

src/features/constraintMenu/AutoCompletion.ts

Lines changed: 115 additions & 53 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,138 +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-
private setContent(line: string) {
107-
if (!line) {
108-
line = "";
109-
}
110-
if (line.length == 0) {
111-
this.content = [];
112-
this.length = 0;
113-
return;
101+
private tokenize(text: string[]): Token[] {
102+
if (!text || text.length == 0) {
103+
return [];
114104
}
115-
this.content = line.split(" ");
116-
this.startColumns = this.content.map(() => 0);
117-
for (let i = 1; i < this.content.length; i++) {
118-
this.startColumns[i] = this.startColumns[i - 1] + this.content[i - 1].length + 1;
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+
}
119118
}
120-
this.length = line.length;
119+
120+
return tokens;
121121
}
122122

123123
/**
124124
* Checks the set content for errors
125125
* @returns An array of errors. An empty array means that the content is valid
126126
*/
127-
public verify(line: string): ValidationError[] {
128-
this.setContent(line);
129-
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);
130130
}
131131

132-
private verifyNode(nodes: AutoCompleteNode[], index: number, comesFromFinal: boolean): ValidationError[] {
133-
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) {
134140
if (nodes.length == 0 || comesFromFinal) {
135141
return [];
136142
} else {
137-
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);
138157
}
139158
}
140159

141160
const foundErrors: ValidationError[] = [];
142161
let childErrors: ValidationError[] = [];
143162
for (const n of nodes) {
144-
const v = n.word.verifyWord(this.content[index]);
163+
const v = n.word.verifyWord(tokens[index].text);
145164
if (v.length > 0) {
146165
foundErrors.push({
147166
message: v[0],
148-
startColumn: this.startColumns[index],
149-
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,
150170
});
151171
continue;
152172
}
153173

154-
const childResult = this.verifyNode(n.children, index + 1, n.canBeFinal || false);
174+
const childResult = this.verifyNode(n.children, tokens, index + 1, n.canBeFinal || false);
155175
if (childResult.length == 0) {
156176
return [];
157177
} else {
158178
childErrors = childErrors.concat(childResult);
159179
}
160180
}
161181
if (childErrors.length > 0) {
162-
return childErrors;
182+
return deduplicateErrors(childErrors);
163183
}
164-
return foundErrors;
184+
return deduplicateErrors(foundErrors);
165185
}
166186

167187
/**
168188
* Calculates the completion options for the current content
169189
*/
170-
public getCompletion(line: string, lineNumber = 1): monaco.languages.CompletionItem[] {
171-
this.setContent(line);
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+
}
172202
let result: WordCompletion[] = [];
173-
if (this.content.length == 0) {
203+
if (tokens.length == 0) {
174204
for (const r of this.roots) {
175205
result = result.concat(r.word.completionOptions(""));
176206
}
177207
} else {
178-
result = this.completeNode(this.roots, 0);
208+
result = this.completeNode(this.roots, tokens, 0);
179209
}
180-
return this.transformResults(result, lineNumber);
210+
return this.transformResults(result, tokens);
181211
}
182212

183-
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+
184228
let result: WordCompletion[] = [];
185-
if (index == this.content.length - 1) {
229+
if (index == tokens.length - 1) {
186230
for (const node of nodes) {
187-
result = result.concat(node.word.completionOptions(this.content[index]));
231+
result = result.concat(node.word.completionOptions(tokens[index].text));
188232
}
189233
return result;
190234
}
191235
for (const n of nodes) {
192-
if (!n.word.verifyWord(this.content[index])) {
236+
if (!n.word.verifyWord(tokens[index].text)) {
193237
continue;
194238
}
195-
result = result.concat(this.completeNode(n.children, index + 1));
239+
result = result.concat(this.completeNode(n.children, tokens, index + 1));
196240
}
197241
return result;
198242
}
199243

200-
private transformResults(comp: WordCompletion[], lineNumber = 1): monaco.languages.CompletionItem[] {
244+
private transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] {
201245
const result: monaco.languages.CompletionItem[] = [];
202246
const filtered = comp.filter(
203247
(c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx,
204248
);
205249
for (const c of filtered) {
206-
const r = this.transformResult(c, lineNumber);
250+
const r = this.transformResult(c, tokens);
207251
result.push(r);
208252
}
209253
return result;
210254
}
211255

212-
private transformResult(comp: WordCompletion, lineNumber = 1): monaco.languages.CompletionItem {
213-
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;
214259
return {
215260
insertText: comp.insertText,
216261
kind: comp.kind,
217262
label: comp.label ?? comp.insertText,
218263
insertTextRules: comp.insertTextRules,
219-
range: new monaco.Range(lineNumber, wordStart + (comp.startOffset ?? 0), lineNumber, 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+
),
220270
};
221271
}
222272
}
223273

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+
224286
export interface AutoCompleteNode {
225287
word: AbstractWord;
226288
children: AutoCompleteNode[];

src/features/constraintMenu/ConstraintMenu.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,32 +137,29 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
137137
return;
138138
}
139139

140-
this.constraintRegistry.setConstraints(this.editor.getValue());
141-
142140
const model = this.editor?.getModel();
143141
if (!model) {
144142
return;
145143
}
146144

147-
const content = model.getValue();
145+
this.constraintRegistry.setConstraints(model.getLinesContent());
146+
147+
const content = model.getLinesContent();
148148
const marker: monaco.editor.IMarkerData[] = [];
149+
const emptyContent = content.length == 0 || (content.length == 1 && content[0] === "");
149150
// empty content gets accepted as valid as it represents no constraints
150-
if (content !== "") {
151-
const lines = this.editor.getValue().split(/\r?\n/gm);
152-
const errors = lines.map(this.tree.verify, this.tree);
153-
for (let i = 0; i < errors.length; i++) {
154-
const lineErrors = errors[i];
155-
marker.push(
156-
...lineErrors.map((e) => ({
157-
severity: monaco.MarkerSeverity.Error,
158-
startLineNumber: i + 1,
159-
startColumn: e.startColumn + 1,
160-
endLineNumber: i + 1,
161-
endColumn: e.endColumn + 1,
162-
message: e.message,
163-
})),
164-
);
165-
}
151+
if (!emptyContent) {
152+
const errors = this.tree.verify(content);
153+
marker.push(
154+
...errors.map((e) => ({
155+
severity: monaco.MarkerSeverity.Error,
156+
startLineNumber: e.line,
157+
startColumn: e.startColumn,
158+
endLineNumber: e.line,
159+
endColumn: e.endColumn,
160+
message: e.message,
161+
})),
162+
);
166163
}
167164

168165
this.validationLabel.innerText =

src/features/constraintMenu/DslLanguage.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ export class MonacoEditorConstraintDslCompletionProvider implements monaco.langu
2424
model: monaco.editor.ITextModel,
2525
position: monaco.Position,
2626
): monaco.languages.ProviderResult<monaco.languages.CompletionList> {
27-
const r = this.tree.getCompletion(
28-
model.getLineContent(position.lineNumber).substring(0, position.column - 1),
29-
position.lineNumber,
30-
);
27+
const allLines = model.getLinesContent();
28+
const includedLines: string[] = [];
29+
for (let i = 0; i < position.lineNumber - 1; i++) {
30+
includedLines.push(allLines[i]);
31+
}
32+
const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1);
33+
includedLines.push(currentLine);
34+
35+
const r = this.tree.getCompletion(includedLines);
3136
return {
3237
suggestions: r,
3338
};

0 commit comments

Comments
 (0)