stack traces in the brush editor
after 35 thousand years it's finally here good erro message
This commit is contained in:
parent
c1612b2a94
commit
e49885c83a
11 changed files with 710 additions and 150 deletions
|
@ -1,4 +1,4 @@
|
|||
import { CodeEditor } from "rkgk/code-editor.js";
|
||||
import { CodeEditor, Selection } from "rkgk/code-editor.js";
|
||||
import { SaveData } from "rkgk/framework.js";
|
||||
import { builtInPresets } from "rkgk/brush-box.js";
|
||||
|
||||
|
@ -33,6 +33,10 @@ export class BrushEditor extends HTMLElement {
|
|||
|
||||
this.codeEditor = this.appendChild(
|
||||
new CodeEditor([
|
||||
{
|
||||
className: "layer-syntax",
|
||||
render: (code, element) => this.#renderSyntax(code, element),
|
||||
},
|
||||
{
|
||||
className: "layer-error-squiggles",
|
||||
render: (code, element) => this.#renderErrorSquiggles(code, element),
|
||||
|
@ -116,8 +120,32 @@ export class BrushEditor extends HTMLElement {
|
|||
} else if (result.errorKind == "plain") {
|
||||
this.errorHeader.textContent = result.message;
|
||||
} else if (result.errorKind == "exception") {
|
||||
// TODO: This should show a stack trace.
|
||||
this.errorArea.textContent = `an exception occurred: ${result.message}`;
|
||||
let renderer = new ErrorException(result);
|
||||
let squiggles = renderer.prepareSquiggles();
|
||||
|
||||
this.codeEditor.rebuildLineMap();
|
||||
this.errorSquiggles = this.#computeErrorSquiggles(
|
||||
this.codeEditor.lineMap,
|
||||
squiggles.diagnostics,
|
||||
);
|
||||
this.codeEditor.renderLayer("layer-error-squiggles");
|
||||
|
||||
renderer.addEventListener(".functionNameMouseEnter", (event) => {
|
||||
squiggles.highlightSegment(event.frameIndex);
|
||||
});
|
||||
|
||||
renderer.addEventListener(".functionNameMouseLeave", () => {
|
||||
squiggles.highlightSegment(null);
|
||||
});
|
||||
|
||||
renderer.addEventListener(".functionNameClick", (event) => {
|
||||
let span = event.frame.span;
|
||||
this.codeEditor.textArea.focus();
|
||||
this.codeEditor.setSelection(new Selection(span.start, span.end));
|
||||
});
|
||||
|
||||
this.errorArea.replaceChildren();
|
||||
this.errorArea.appendChild(renderer);
|
||||
} else {
|
||||
console.warn(`unknown error kind: ${result.errorKind}`);
|
||||
this.errorHeader.textContent = "(unknown error kind)";
|
||||
|
@ -206,6 +234,10 @@ export class BrushEditor extends HTMLElement {
|
|||
let spanElement = lineElement.appendChild(document.createElement("span"));
|
||||
spanElement.classList.add("squiggle", "squiggle-error");
|
||||
spanElement.textContent = text;
|
||||
|
||||
for (let diagnostic of segment.diagnostics) {
|
||||
diagnostic.customizeSegment?.(diagnostic, spanElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -213,6 +245,151 @@ export class BrushEditor extends HTMLElement {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#renderSyntax(lines, element) {
|
||||
// TODO: Syntax highlighting.
|
||||
// Right now we just render a layer of black text to have the text visible in the code editor.
|
||||
element.textContent = lines.string;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rkgk-brush-editor", BrushEditor);
|
||||
|
||||
class ErrorException extends HTMLElement {
|
||||
constructor(result) {
|
||||
super();
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
prepareSquiggles() {
|
||||
let diagnostics = [];
|
||||
let segments = [];
|
||||
|
||||
const customizeSegment = (diagnostic, segment) => {
|
||||
// The control flow here is kind of spaghetti, but basically this fills out `segments`
|
||||
// as the squiggles are being rendered.
|
||||
// Once squiggles are rendered, you're free to call highlightSegment(i).
|
||||
segments.push({
|
||||
frameIndex: diagnostic.frameIndex,
|
||||
colorIndex: diagnostic.colorIndex,
|
||||
segment,
|
||||
});
|
||||
if (diagnostic.colorIndex != null) {
|
||||
segment.classList.add("squiggle-colored");
|
||||
segment.style.setProperty("--color-index", diagnostic.colorIndex);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < this.result.stackTrace.length; ++i) {
|
||||
let frame = this.result.stackTrace[i];
|
||||
if (frame.span != null) {
|
||||
diagnostics.push({
|
||||
start: frame.span.start,
|
||||
end: frame.span.end,
|
||||
|
||||
customizeSegment,
|
||||
frameIndex: i,
|
||||
colorIndex: i / this.result.stackTrace.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
diagnostics,
|
||||
highlightSegment(frameIndex) {
|
||||
let justHighlighted = new Set();
|
||||
for (let entry of segments.values()) {
|
||||
let segment = entry.segment;
|
||||
if (entry.frameIndex == frameIndex) {
|
||||
segment.classList.add("squiggle-highlighted");
|
||||
// This logic exists so that a specific segment can display on top of
|
||||
// another one if it's highlighted.
|
||||
segment.style.setProperty("--highlight-color-index", entry.colorIndex);
|
||||
justHighlighted.add(segment);
|
||||
} else if (!justHighlighted.has(segment)) {
|
||||
segment.classList.remove("squiggle-highlighted");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
let message = this.appendChild(document.createElement("p"));
|
||||
message.classList.add("message");
|
||||
message.textContent = this.result.message;
|
||||
|
||||
let stackTrace = this.appendChild(document.createElement("ol"));
|
||||
stackTrace.classList.add("stack-trace");
|
||||
for (let i = 0; i < this.result.stackTrace.length; ++i) {
|
||||
let frame = this.result.stackTrace[i];
|
||||
let line = stackTrace.appendChild(document.createElement("li"));
|
||||
|
||||
let [_, name, lambdasString] = frame.functionName.match(/([^λ]+)(λ*)/);
|
||||
|
||||
let inCaption = line.appendChild(document.createElement("span"));
|
||||
inCaption.classList.add("in");
|
||||
inCaption.innerText = "in ";
|
||||
|
||||
let functionName = line.appendChild(document.createElement("button"));
|
||||
functionName.classList.add("function-name");
|
||||
functionName.disabled = true;
|
||||
functionName.innerText = name;
|
||||
if (name == "(brush)") {
|
||||
functionName.classList.add("function-name-brush");
|
||||
functionName.title = "Top-level brush code";
|
||||
}
|
||||
let lambdas = functionName.appendChild(document.createElement("abbr"));
|
||||
lambdas.classList.add("lambdas");
|
||||
lambdas.textContent = lambdasString;
|
||||
lambdas.title =
|
||||
"λ - unnamed function\n" +
|
||||
"Symbolizes functions that were not given a name.\n" +
|
||||
"Each additional λ is one level of nesting inside another function.";
|
||||
|
||||
if (frame.isSystem) {
|
||||
line.append(" ");
|
||||
let system = line.appendChild(document.createElement("abbr"));
|
||||
system.classList.add("system");
|
||||
system.innerText = "(built-in)";
|
||||
system.title =
|
||||
"This function is built into rakugaki and does not exist in your brush's source code, so you can't see it in the editor.";
|
||||
}
|
||||
|
||||
if (frame.span != null) {
|
||||
functionName.disabled = false;
|
||||
functionName.classList.add("source-link");
|
||||
functionName.style.setProperty("--color-index", i / this.result.stackTrace.length);
|
||||
|
||||
functionName.addEventListener("mouseenter", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameMouseEnter"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
functionName.addEventListener("mouseleave", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameMouseLeave"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
functionName.addEventListener("click", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameClick"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rkgk-brush-editor-error-exception", ErrorException);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue