Skip to content

Commit 57f5c19

Browse files
Merge pull request #463 from SwiftFiddle/error-location
Error location
2 parents b154fe1 + d5095c2 commit 57f5c19

File tree

12 files changed

+258
-78
lines changed

12 files changed

+258
-78
lines changed

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Public/css/highlight.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
color: #c0c;
8686
}
8787

88+
.exp-error {
89+
background: #d22;
90+
}
91+
8892
span.match-char {
8993
background: rgba(112, 176, 224, 0.5);
9094
color: #101112;

Public/js/app.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ export class App {
227227

228228
onExpressionFieldChange() {
229229
if (!this.expressionField.value) {
230+
this.expressionField.tokens = [];
231+
this.expressionField.error = null;
232+
this.dslView.value = "";
233+
this.dslView.error = null;
230234
this.updateMatchCount(0, "match-count");
231235
return;
232236
}
@@ -349,13 +353,35 @@ export class App {
349353
} else {
350354
this.expressionField.tokens = [];
351355
}
352-
this.expressionField.error = response.error;
356+
if (response.error) {
357+
try {
358+
const error = JSON.parse(response.error);
359+
if (error) {
360+
this.expressionField.error = error;
361+
}
362+
} catch (e) {
363+
this.expressionField.error = response.error;
364+
}
365+
} else {
366+
this.expressionField.error = null;
367+
}
353368
break;
354369
case "convertToDSL":
355370
if (response.result) {
356371
this.dslView.value = JSON.parse(response.result);
357372
}
358-
this.dslView.error = response.error;
373+
if (response.error) {
374+
try {
375+
const error = JSON.parse(response.error);
376+
if (error) {
377+
this.dslView.error = error;
378+
}
379+
} catch (e) {
380+
this.dslView.error = response.error;
381+
}
382+
} else {
383+
this.dslView.error = null;
384+
}
359385
break;
360386
case "match":
361387
if (response.result) {

Public/js/views/dsl_view.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class DSLView extends EventDispatcher {
1616
}
1717

1818
set value(val) {
19-
this.editor.setValue(val || this.defaultText);
19+
this.editor.setValue(val);
2020
}
2121

2222
set error(error) {
@@ -32,14 +32,26 @@ export class DSLView extends EventDispatcher {
3232
if (!error) {
3333
return;
3434
}
35-
36-
widgets.push(
37-
editor.addLineWidget(0, ErrorMessage.create(error), {
38-
coverGutter: false,
39-
noHScroll: true,
40-
above: true,
41-
})
42-
);
35+
if (typeof error === "string" && error instanceof String) {
36+
widgets.push(
37+
editor.addLineWidget(0, ErrorMessage.create(error), {
38+
coverGutter: false,
39+
noHScroll: true,
40+
above: true,
41+
})
42+
);
43+
} else {
44+
for (const e of error) {
45+
const message = ErrorMessage.create(e.message);
46+
widgets.push(
47+
editor.addLineWidget(0, message, {
48+
coverGutter: false,
49+
noHScroll: true,
50+
above: true,
51+
})
52+
);
53+
}
54+
}
4355
});
4456
}
4557

Public/js/views/expression_field.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,34 @@ export class ExpressionField extends EventDispatcher {
3030

3131
set error(error) {
3232
if (error) {
33-
const content = Utils.htmlSafe(error);
34-
this.errorMessageTooltip.setContent(
35-
`<span class="fw-bolder text-danger">Parse Error:</span> ${content}`
36-
);
33+
let message = "";
34+
if (typeof error === "string" && error instanceof String) {
35+
const errorMessage = Utils.htmlSafe(error);
36+
message = `<span class="fw-bolder text-danger">Parse Error:</span> ${errorMessage}`;
37+
} else {
38+
message = error
39+
.map((e) => {
40+
const errorMessage = Utils.htmlSafe(e.message);
41+
return `<span class="fw-bolder text-danger">${e.behavior}:</span> ${errorMessage}`;
42+
})
43+
.join("<br>");
44+
this.highlighter.drawError(error);
45+
}
46+
this.errorMessageTooltip.setContent(message);
3747
document
3848
.getElementById("expression-field-error")
3949
.classList.remove("d-none");
4050
} else {
4151
this.errorMessageTooltip.setContent("");
4252
document.getElementById("expression-field-error").classList.add("d-none");
53+
this.highlighter.clearError();
4354
}
55+
56+
tippy(".exp-error", {
57+
allowHTML: true,
58+
animation: false,
59+
placement: "bottom",
60+
});
4461
}
4562

4663
init(container) {
@@ -85,6 +102,9 @@ export class ExpressionField extends EventDispatcher {
85102
...tooltipProps,
86103
onShow: (instance) => {
87104
const index = instance.reference.dataset.tokenIndex;
105+
if (index === undefined) {
106+
return false;
107+
}
88108
const token = this.expressionTokens[index];
89109
this.onHover(token, instance);
90110
return false;

Public/js/views/expression_highlighter.js

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default class ExpressionHighlighter extends EventDispatcher {
1010
this.editor = editor;
1111
this.activeMarks = [];
1212
this.hoverMarks = [];
13+
this.widgets = [];
1314
}
1415

1516
draw(tokens) {
@@ -60,13 +61,52 @@ export default class ExpressionHighlighter extends EventDispatcher {
6061
});
6162
}
6263

64+
drawError(errors) {
65+
this.clearError();
66+
67+
const pre = ExpressionHighlighter.CSS_PREFIX;
68+
const editor = this.editor;
69+
editor.operation(() => {
70+
for (const error of errors) {
71+
const location = Editor.calcRangePos(
72+
this.editor,
73+
error.location.start,
74+
error.location.end - error.location.start
75+
);
76+
const widget = document.createElement("span");
77+
widget.className = `${pre}-error`;
78+
79+
widget.style.height = `2px`;
80+
widget.style.zIndex = "10";
81+
widget.setAttribute("data-tippy-content", error.message);
82+
83+
editor.addWidget(location.startPos, widget);
84+
const startCoords = editor.charCoords(location.startPos, "local");
85+
const endCoords = editor.charCoords(location.endPos, "local");
86+
widget.style.left = `${startCoords.left + 1}px`;
87+
widget.style.top = `${startCoords.bottom}px`;
88+
widget.style.width = `${endCoords.left - startCoords.left - 2}px`;
89+
90+
this.widgets.push(widget);
91+
}
92+
});
93+
}
94+
6395
clear() {
6496
this.editor.operation(() => {
65-
let marks = this.activeMarks;
66-
for (var i = 0, l = marks.length; i < l; i++) {
67-
marks[i].clear();
97+
for (const mark of this.activeMarks) {
98+
mark.clear();
99+
}
100+
this.activeMarks.length = 0;
101+
});
102+
}
103+
104+
clearError() {
105+
this.editor.operation(() => {
106+
for (const widget of this.widgets) {
107+
widget.parentNode.removeChild(widget);
68108
}
69-
marks.length = 0;
109+
this.widgets.length = 0;
70110
});
71111
}
72112

@@ -77,9 +117,7 @@ export default class ExpressionHighlighter extends EventDispatcher {
77117
return;
78118
}
79119

80-
while (this.hoverMarks.length) {
81-
this.hoverMarks.pop().clear();
82-
}
120+
this.clearHover();
83121

84122
if (selection) {
85123
this.drawBorder(selection, "selected");
@@ -119,9 +157,12 @@ export default class ExpressionHighlighter extends EventDispatcher {
119157
}
120158

121159
clearHover() {
122-
while (this.hoverMarks.length) {
123-
this.hoverMarks.pop().clear();
124-
}
160+
this.editor.operation(() => {
161+
for (const mark of this.hoverMarks) {
162+
mark.clear();
163+
}
164+
this.hoverMarks.length = 0;
165+
});
125166
}
126167
}
127168

Sources/DSLConverter/DSLConverter.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import Foundation
33
@testable @_spi(RegexBuilder) import _StringProcessing
44
@testable @_spi(PatternConverter) import _StringProcessing
55

6-
struct DSLConverter {
6+
class DSLConverter {
7+
private(set) var diagnostics: Diagnostics?
8+
79
func convert(_ pattern: String, matchingOptions: [String] = []) throws -> String {
8-
let ast = try _RegexParser.parse(pattern, .traditional)
10+
let ast = _RegexParser.parseWithRecovery(pattern, .traditional)
11+
diagnostics = ast.diags
12+
913
var builderDSL = renderAsBuilderDSL(ast: ast)
1014
if builderDSL.last == "\n" {
1115
builderDSL = String(builderDSL.dropLast())

Sources/DSLConverter/Main.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,34 @@ struct Main {
1414

1515
let data = try JSONEncoder().encode(builderDSL)
1616
print(String(data: data, encoding: .utf8) ?? "")
17+
18+
if let diagnostics = converter.diagnostics {
19+
let errors = diagnostics.diags.map {
20+
let location = $0.location
21+
let (start, end) = (location.start, location.end)
22+
23+
let behavior = switch $0.behavior {
24+
case .fatalError:
25+
"Fatal Error"
26+
case .error:
27+
"Error"
28+
case .warning:
29+
"Warning"
30+
}
31+
return LocatedMessage(
32+
behavior: behavior,
33+
message: $0.message,
34+
location: Location(
35+
start: start.utf16Offset(in: pattern), end: end.utf16Offset(in: pattern)
36+
)
37+
)
38+
}
39+
40+
let data = try JSONEncoder().encode(errors)
41+
print(String(data: data, encoding: .utf8) ?? "", to: &standardError)
42+
}
1743
} catch {
18-
print("\(error)", to:&standardError)
44+
print("\(error)", to: &standardError)
1945
}
2046
}
2147
}
@@ -28,3 +54,14 @@ extension FileHandle: @retroactive TextOutputStream {
2854
self.write(data)
2955
}
3056
}
57+
58+
struct Location: Codable {
59+
let start: Int
60+
let end: Int
61+
}
62+
63+
struct LocatedMessage: Codable {
64+
let behavior: String
65+
let message: String
66+
let location: Location
67+
}

Sources/ExpressionParser/Main.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,37 @@ struct Main {
1212
var parser = ExpressionParser(pattern: pattern, matchingOptions: matchingOptions)
1313
parser.parse()
1414

15-
let data = try JSONEncoder().encode(parser.tokens)
15+
let encoder = JSONEncoder()
16+
let data = try encoder.encode(parser.tokens)
1617
print(String(data: data, encoding: .utf8) ?? "")
18+
1719
if let diagnostics = parser.diagnostics {
18-
for diag in diagnostics.diags {
19-
print("\(diag.message)", to:&standardError)
20+
let errors = diagnostics.diags.map {
21+
let location = $0.location
22+
let (start, end) = (location.start, location.end)
23+
24+
let behavior = switch $0.behavior {
25+
case .fatalError:
26+
"Fatal Error"
27+
case .error:
28+
"Error"
29+
case .warning:
30+
"Warning"
31+
}
32+
return LocatedMessage(
33+
behavior: behavior,
34+
message: $0.message,
35+
location: Location(
36+
start: start.utf16Offset(in: pattern), end: end.utf16Offset(in: pattern)
37+
)
38+
)
2039
}
40+
41+
let data = try JSONEncoder().encode(errors)
42+
print(String(data: data, encoding: .utf8) ?? "", to: &standardError)
2143
}
2244
} catch {
23-
print("\(error)", to:&standardError)
45+
print("\(error)", to: &standardError)
2446
}
2547
}
2648
}
@@ -33,3 +55,9 @@ extension FileHandle: @retroactive TextOutputStream {
3355
self.write(data)
3456
}
3557
}
58+
59+
struct LocatedMessage: Codable {
60+
let behavior: String
61+
let message: String
62+
let location: Location
63+
}

Sources/Matcher/Main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct Main {
2222
let data = try JSONEncoder().encode(matches)
2323
print(String(data: data, encoding: .utf8) ?? "")
2424
} catch {
25-
print("\(error)", to:&standardError)
25+
print("\(error)", to: &standardError)
2626
}
2727
}
2828
}

0 commit comments

Comments
 (0)