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

Commit 9eb47d3

Browse files
committed
better multiline
1 parent 5d6b12c commit 9eb47d3

3 files changed

Lines changed: 116 additions & 126 deletions

File tree

src/features/constraintMenu/AutoCompletion.ts

Lines changed: 95 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -96,174 +96,166 @@ export class NegatableWord implements AbstractWord {
9696
}
9797

9898
export class AutoCompleteTree {
99-
private content: Token[];
99+
constructor(private roots: AutoCompleteNode[]) {}
100100

101-
constructor(private roots: AutoCompleteNode[]) {
102-
this.content = [];
103-
}
104-
105-
/**
106-
* Sets the content of the tree for the next analyzing cycle
107-
*/
108-
private setContent(text: string) {
109-
if (!text) {
110-
text = "";
111-
}
112-
if (text.length == 0) {
113-
this.content = [];
114-
return;
101+
private tokenize(text: string[]): Token[] {
102+
if (!text || text.length == 0) {
103+
return [];
115104
}
116105

117-
let currentToken = "";
118-
let currentLine = 1;
119-
let currentColumn = 0;
120-
this.content = [];
121-
let index = 0;
122-
while (index < text.length) {
123-
const char = text[index];
124-
if (char === "\n") {
125-
if (currentToken.length > 0) {
126-
this.content.push({
127-
text: currentToken,
128-
line: currentLine,
129-
column: currentColumn - currentToken.length + 1,
130-
});
131-
}
132-
currentToken = "";
133-
currentLine++;
134-
currentColumn = 1;
135-
} else if (char === " " || char === "\t") {
136-
if (currentToken.length > 0) {
137-
this.content.push({
138-
text: currentToken,
139-
line: currentLine,
140-
column: currentColumn - currentToken.length + 1,
141-
});
142-
}
143-
currentToken = "";
144-
currentColumn += 1;
145-
} else {
146-
currentToken += char;
147-
currentColumn += 1;
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+
});
148117
}
149-
index++;
150-
}
151-
if (currentToken.length > 0) {
152-
this.content.push({
153-
text: currentToken,
154-
line: currentLine,
155-
column: currentColumn - currentToken.length + 1,
156-
});
157118
}
158-
this.content = this.content.map((c) => ({ ...c, text: c.text.trim() })).filter((c) => c.text.length > 0);
119+
120+
return tokens;
159121
}
160122

161123
/**
162124
* Checks the set content for errors
163125
* @returns An array of errors. An empty array means that the content is valid
164126
*/
165-
public verify(line: string): ValidationError[] {
166-
this.setContent(line);
167-
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);
168130
}
169131

170-
private verifyNode(nodes: AutoCompleteNode[], index: number, comesFromFinal: boolean): ValidationError[] {
171-
if (comesFromFinal && this.content[index].column == 0) {
172-
const checkStart = this.verifyNode(this.roots, index, true);
173-
if (checkStart.length > 0) {
174-
return checkStart;
175-
}
176-
}
177-
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) {
178140
if (nodes.length == 0 || comesFromFinal) {
179141
return [];
180142
} else {
181143
return [
182144
{
183145
message: "Unexpected end of line",
184-
line: this.content[index - 1].line,
185-
startColumn: this.content[index - 1].column + this.content[index - 1].text.length - 1,
186-
endColumn: this.content[index - 1].column + this.content[index - 1].text.length,
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,
187149
},
188150
];
189151
}
190152
}
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);
157+
}
158+
}
191159

192160
const foundErrors: ValidationError[] = [];
193161
let childErrors: ValidationError[] = [];
194162
for (const n of nodes) {
195-
const v = n.word.verifyWord(this.content[index].text);
163+
const v = n.word.verifyWord(tokens[index].text);
196164
if (v.length > 0) {
197165
foundErrors.push({
198166
message: v[0],
199-
startColumn: this.content[index].column,
200-
endColumn: this.content[index].column + this.content[index].text.length,
201-
line: this.content[index].line,
167+
startColumn: tokens[index].column,
168+
endColumn: tokens[index].column + tokens[index].text.length,
169+
line: tokens[index].line,
202170
});
203171
continue;
204172
}
205173

206-
const childResult = this.verifyNode(n.children, index + 1, n.canBeFinal || false);
174+
const childResult = this.verifyNode(n.children, tokens, index + 1, n.canBeFinal || false);
207175
if (childResult.length == 0) {
208176
return [];
209177
} else {
210178
childErrors = childErrors.concat(childResult);
211179
}
212180
}
213181
if (childErrors.length > 0) {
214-
return childErrors;
182+
return deduplicateErrors(childErrors);
215183
}
216-
return foundErrors;
184+
return deduplicateErrors(foundErrors);
217185
}
218186

219187
/**
220188
* Calculates the completion options for the current content
221189
*/
222-
public getCompletion(line: string): monaco.languages.CompletionItem[] {
223-
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+
}
224202
let result: WordCompletion[] = [];
225-
if (this.content.length == 0) {
203+
if (tokens.length == 0) {
226204
for (const r of this.roots) {
227205
result = result.concat(r.word.completionOptions(""));
228206
}
229207
} else {
230-
result = this.completeNode(this.roots, 0);
208+
result = this.completeNode(this.roots, tokens, 0);
231209
}
232-
return this.transformResults(result);
210+
return this.transformResults(result, tokens);
233211
}
234212

235-
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+
236228
let result: WordCompletion[] = [];
237-
if (index == this.content.length - 1) {
229+
if (index == tokens.length - 1) {
238230
for (const node of nodes) {
239-
result = result.concat(node.word.completionOptions(this.content[index].text));
231+
result = result.concat(node.word.completionOptions(tokens[index].text));
240232
}
241233
return result;
242234
}
243235
for (const n of nodes) {
244-
if (!n.word.verifyWord(this.content[index].text)) {
236+
if (!n.word.verifyWord(tokens[index].text)) {
245237
continue;
246238
}
247-
result = result.concat(this.completeNode(n.children, index + 1));
239+
result = result.concat(this.completeNode(n.children, tokens, index + 1));
248240
}
249241
return result;
250242
}
251243

252-
private transformResults(comp: WordCompletion[]): monaco.languages.CompletionItem[] {
244+
private transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] {
253245
const result: monaco.languages.CompletionItem[] = [];
254246
const filtered = comp.filter(
255247
(c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx,
256248
);
257249
for (const c of filtered) {
258-
const r = this.transformResult(c);
250+
const r = this.transformResult(c, tokens);
259251
result.push(r);
260252
}
261253
return result;
262254
}
263255

264-
private transformResult(comp: WordCompletion): monaco.languages.CompletionItem {
265-
const wordStart = this.content.length == 0 ? 1 : this.content[this.content.length - 1].column - 1;
266-
const lineNumber = this.content.length == 0 ? 1 : this.content[this.content.length - 1].line;
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;
267259
return {
268260
insertText: comp.insertText,
269261
kind: comp.kind,
@@ -279,6 +271,18 @@ export class AutoCompleteTree {
279271
}
280272
}
281273

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+
282286
export interface AutoCompleteNode {
283287
word: AbstractWord;
284288
children: AutoCompleteNode[];

src/features/constraintMenu/ConstraintMenu.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -138,31 +138,22 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
138138
return;
139139
}
140140

141-
const content = model.getValue();
141+
const content = model.getLinesContent();
142142
const marker: monaco.editor.IMarkerData[] = [];
143+
const emptyContent = content.length == 0 || (content.length == 1 && content[0] === "");
143144
// empty content gets accepted as valid as it represents no constraints
144-
if (content !== "") {
145-
const constraintTexts = ConstraintRegistry.splitToConstraintTexts(content);
146-
const errors = constraintTexts.map(
147-
(c) => ({
148-
errors: this.tree.verify(c.text),
149-
c,
150-
}),
151-
this.tree,
145+
if (!emptyContent) {
146+
const errors = this.tree.verify(content);
147+
marker.push(
148+
...errors.map((e) => ({
149+
severity: monaco.MarkerSeverity.Error,
150+
startLineNumber: e.line,
151+
startColumn: e.startColumn,
152+
endLineNumber: e.line,
153+
endColumn: e.endColumn,
154+
message: e.message,
155+
})),
152156
);
153-
for (let i = 0; i < errors.length; i++) {
154-
const lineErrors = errors[i];
155-
marker.push(
156-
...lineErrors.errors.map((e) => ({
157-
severity: monaco.MarkerSeverity.Error,
158-
startLineNumber: e.line + lineErrors.c.line,
159-
startColumn: e.startColumn,
160-
endLineNumber: e.line + lineErrors.c.line,
161-
endColumn: e.endColumn,
162-
message: e.message,
163-
})),
164-
);
165-
}
166157
}
167158

168159
this.validationLabel.innerText =

src/features/constraintMenu/DslLanguage.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +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 lines = model.getLinesContent();
28-
let curText = "";
29-
if (lines[position.lineNumber - 1].startsWith("-")) {
30-
curText = lines[position.lineNumber - 1].substring(1, position.column - 1);
31-
} else {
32-
curText = lines[position.lineNumber - 1].substring(0, position.column - 1);
33-
for (let i = position.lineNumber - 2; i >= 0; i--) {
34-
curText = lines[i] + "\n" + curText;
35-
if (lines[i].startsWith("-")) {
36-
break;
37-
}
38-
}
27+
const allLines = model.getLinesContent();
28+
const includedLines: string[] = [];
29+
for (let i = 0; i < position.lineNumber - 1; i++) {
30+
includedLines.push(allLines[i]);
3931
}
40-
const r = this.tree.getCompletion(model.getLineContent(position.lineNumber).substring(0, position.column - 1));
32+
const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1);
33+
includedLines.push(currentLine);
34+
35+
const r = this.tree.getCompletion(includedLines);
4136
return {
4237
suggestions: r,
4338
};

0 commit comments

Comments
 (0)