Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧾 Stack traces: code preview, URL, truncated, design #2813

Merged
merged 9 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 142 additions & 27 deletions frontend/components/ErrorMessage.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,117 @@
import { cl } from "../common/ClassTable.js"
import { PlutoActionsContext } from "../common/PlutoContext.js"
import { EditorState, EditorView, julia_andrey, lineNumbers, syntaxHighlighting } from "../imports/CodemirrorPlutoSetup.js"
import { html, useContext, useEffect, useLayoutEffect, useRef, useState } from "../imports/Preact.js"
import { pluto_syntax_colors } from "./CellInput.js"
import { highlight } from "./CellOutput.js"
import { Editor } from "./Editor.js"

const StackFrameFilename = ({ frame, cell_id }) => {
const sep_index = frame.file.indexOf("#==#")
const extract_cell_id = (/** @type {string} */ file) => {
const sep_index = file.indexOf("#==#")
if (sep_index != -1) {
const frame_cell_id = frame.file.substr(sep_index + 4, 36)
return file.substring(sep_index + 4, sep_index + 4 + 36)
} else {
return null
}
}

const focus_line = (cell_id, line) =>
window.dispatchEvent(
new CustomEvent("cell_focus", {
detail: {
cell_id: cell_id,
line: line,
},
})
)

const StackFrameFilename = ({ frame, cell_id }) => {
if (ignore_location(frame)) return null

const frame_cell_id = extract_cell_id(frame.file)
if (frame_cell_id != null) {
const a = html`<a
internal-file=${frame.file}
href="#"
href=${`#${frame_cell_id}`}
onclick=${(e) => {
window.dispatchEvent(
new CustomEvent("cell_focus", {
detail: {
cell_id: frame_cell_id,
line: frame.line - 1, // 1-based to 0-based index
},
})
)
focus_line(frame_cell_id, frame.line - 1)
e.preventDefault()
}}
>
${frame_cell_id == cell_id ? "Local" : "Other"}: ${frame.line}
${frame_cell_id == cell_id ? "This cell" : "Other cell"}: line ${frame.line}
</a>`
return html`<em>${a}</em>`
} else {
return html`<em title=${frame.path}>${frame.file}:${frame.line}</em>`
return html`<em title=${frame.path}
><a class="remote-url" href=${frame?.url?.startsWith?.("https") ? frame.url : null}>${frame.file}:${frame.line}</a></em
>`
}
}

const ignore_funccall = (frame) => frame.call === "top-level scope"
const ignore_location = (frame) => frame.file === "none"

const Funccall = ({ frame }) => {
if (ignore_funccall(frame)) return null

const bracket_index = frame.call.indexOf("(")
if (bracket_index != -1) {
return html`<mark><strong>${frame.call.substr(0, bracket_index)}</strong>${frame.call.substr(bracket_index)}</mark>`
} else {
return html`<mark><strong>${frame.call}</strong></mark>`

let inner =
bracket_index != -1
? html`<strong>${frame.call.substr(0, bracket_index)}</strong>${frame.call.substr(bracket_index)}`
: html`<strong>${frame.call}</strong>`

return html`<mark>${inner}</mark><span>@</span>`
}

const LinePreview = ({ frame, num_context_lines = 2 }) => {
let pluto_actions = useContext(PlutoActionsContext)
let cell_id = extract_cell_id(frame.file)
if (cell_id) {
let code = /** @type{import("./Editor.js").NotebookData?} */ (pluto_actions.get_notebook())?.cell_inputs[cell_id]?.code

if (code) {
const lines = code.split("\n")
return html`<a
onclick=${(e) => {
focus_line(cell_id, frame.line - 1)
e.preventDefault()
}}
href=${`#${cell_id}`}
class="frame-line-preview"
><div>
<pre>
${lines.map((line, i) =>
frame.line - 1 - num_context_lines <= i && i <= frame.line - 1 + num_context_lines
? html`<${JuliaHighlightedLine} code=${line} i=${i} frameLine=${i === frame.line - 1} />`
: null
)}</pre
>
</div></a
>`
}
}
}

const JuliaHighlightedLine = ({ code, frameLine, i }) => {
const code_ref = useRef(/** @type {HTMLPreElement?} */ (null))
useLayoutEffect(() => {
if (code_ref.current) {
code_ref.current.innerText = code
highlight(code_ref.current, "julia")
}
}, [code_ref.current, code])

return html`<code
ref=${code_ref}
style=${`--before-content: "${i + 1}";`}
class=${cl({
"language-julia": true,
"frame-line": frameLine,
})}
></code>`
}

const insert_commas_and_and = (/** @type {any[]} */ xs) => xs.flatMap((x, i) => (i === xs.length - 1 ? [x] : i === xs.length - 2 ? [x, " and "] : [x, ", "]))

export const ParseError = ({ cell_id, diagnostics }) => {
Expand All @@ -59,18 +131,22 @@ export const ParseError = ({ cell_id, diagnostics }) => {
<jlerror>
<header><p>Syntax error</p></header>
<section>
<div class="stacktrace-header"><secret-h1>Syntax errors</secret-h1></div>
<ol>
${diagnostics.map(
({ message, from, to, line }) =>
html`<li
class="from_this_notebook from_this_cell"
onmouseenter=${() =>
// NOTE: this could be moved move to `StackFrameFilename`
window.dispatchEvent(new CustomEvent("cell_highlight_range", { detail: { cell_id, from, to } }))}
onmouseleave=${() =>
window.dispatchEvent(new CustomEvent("cell_highlight_range", { detail: { cell_id, from: null, to: null } }))}
>
${message}<span>@</span>
<${StackFrameFilename} frame=${{ file: "#==#" + cell_id, line }} cell_id=${cell_id} />
<div class="classical-frame">
${message}<span>@</span>
<${StackFrameFilename} frame=${{ file: "#==#" + cell_id, line }} cell_id=${cell_id} />
</div>
</li>`
)}
</ol>
Expand Down Expand Up @@ -120,6 +196,7 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => {
<p>${begin_hint}</p>`
}
},
show_stacktrace: () => false,
},
{
pattern: /LoadError: cannot assign a value to variable workspace#\d+\..+ from module workspace#\d+/,
Expand Down Expand Up @@ -183,6 +260,10 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => {
display: default_rewriter.display,
show_stacktrace: () => false,
},
{
pattern: /^\s*$/,
display: () => default_rewriter.display("Error"),
},
{
pattern: /^UndefVarError: (.*) not defined\.?$/,
display: (/** @type{string} */ x) => {
Expand All @@ -191,7 +272,7 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => {

// Verify that the UndefVarError is indeed about a variable from an upstream cell.
const match = x.match(/UndefVarError: (.*) not defined/)
let sym = match?.[1] ?? ""
let sym = (match?.[1] ?? "").replaceAll("`", "")
const undefvar_is_from_upstream = Object.values(notebook?.cell_dependencies ?? {}).some((map) =>
Object.keys(map.downstream_cells_map).includes(sym)
)
Expand Down Expand Up @@ -222,20 +303,54 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => {

const matched_rewriter = rewriters.find(({ pattern }) => pattern.test(msg)) ?? default_rewriter

const [show_more, set_show_more] = useState(false)
useEffect(() => {
set_show_more(false)
}, [msg, stacktrace, cell_id])

const first_stack_from_here = stacktrace.findIndex((frame) => extract_cell_id(frame.file) != null)

const limited = !show_more && first_stack_from_here != -1 && first_stack_from_here < stacktrace.length - 1

const limited_stacktrace = (limited ? stacktrace.slice(0, first_stack_from_here + 1) : stacktrace).filter(
(frame) => !(ignore_location(frame) && ignore_funccall(frame))
)

return html`<jlerror>
<header>${matched_rewriter.display(msg)}</header>
${stacktrace.length == 0 || !(matched_rewriter.show_stacktrace?.() ?? true)
? null
: html`<section>
<div class="stacktrace-header">
<secret-h1>Stack trace</secret-h1>
<p>Here is what happened, the most recent locations are first:</p>
</div>

<ol>
${stacktrace.map(
(frame) =>
html`<li>
${limited_stacktrace.map((frame) => {
const frame_cell_id = extract_cell_id(frame.file)
const from_this_notebook = frame_cell_id != null
const from_this_cell = cell_id === frame_cell_id
return html`<li class=${cl({ from_this_notebook, from_this_cell })}>
<div class="classical-frame">
<${Funccall} frame=${frame} />
<span>@</span>
<${StackFrameFilename} frame=${frame} cell_id=${cell_id} />
</li>`
)}
</div>
${from_this_notebook ? html`<${LinePreview} frame=${frame} num_context_lines=${from_this_cell ? 1 : 2} />` : null}
</li>`
})}
${limited
? html`<li>
<a
href="#"
onClick=${(e) => {
set_show_more(true)
e.preventDefault()
}}
>Show more...</a
>
</li>`
: null}
</ol>
</section>`}
</jlerror>`
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const Notebook = ({
/>`
)}
${cell_outputs_delayed && notebook.cell_order.length >= render_cell_outputs_minimum
? html`<div style="font-family: system-ui; font-style: italic; text-align: center; padding: 5rem 1rem;">Loading...</div>`
? html`<div style="font-family: system-ui; font-style: italic; text-align: center; padding: 5rem 1rem;">Loading more cells...</div>`
: null}
</pluto-notebook>
`
Expand Down
4 changes: 3 additions & 1 deletion frontend/dark_color.css
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
/* jlerror */
--jlerror-header-color: #d9baba;
--jlerror-mark-bg-color: rgb(0 0 0 / 18%);
--jlerror-a-bg-color: rgba(82, 58, 58, 0.5);
--jlerror-a-bg-color: rgba(82, 58, 58, 1);
--jlerror-a-border-left-color: #704141;
--jlerror-mark-color: #b1a9a9;

Expand Down Expand Up @@ -183,6 +183,7 @@
--cm-line-numbers-color: #8d86875e;
--cm-selection-background: hsl(215deg 64% 59% / 48%);
--cm-selection-background-blurred: hsl(215deg 0% 59% / 48%);
--cm-highlighted: #cbceb629;

/* code highlighting */
--cm-editor-text-color: #ffe9fc;
Expand All @@ -206,6 +207,7 @@
--cm-matchingBracket-color: white;
--cm-matchingBracket-bg-color: #c58c237a;
--cm-placeholder-text-color: rgb(255 255 255 / 20%);
--cm-clickable-underline: #5d5f70;

/*autocomplete menu*/
--autocomplete-menu-bg-color: var(--input-context-menu-bg-color);
Expand Down
21 changes: 11 additions & 10 deletions frontend/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ pluto-input .cm-editor .cm-line {

pluto-input .cm-editor span.cm-highlighted-range,
pluto-input .cm-editor .cm-line.cm-highlighted-line {
background-color: #bdbdbd68;
background-color: var(--cm-highlighted);
border-radius: 3px;
}

Expand Down Expand Up @@ -3324,18 +3324,11 @@ Based on "Paraíso (Light)" by Jan T. Sott:
.cm-editor .cm-tooltip.cm-tooltip-autocomplete li.c_from_notebook .cm-completionLabel {
font-weight: bold;
text-decoration: underline;
text-decoration-color: #ced2ef;
text-decoration-color: var(--cm-clickable-underline);
text-decoration-thickness: 3px;
text-decoration-skip-ink: none;
}

@media (prefers-color-scheme: dark) {
[data-pluto-variable],
[data-pluto-variable]:hover {
text-decoration-color: #5d5f70;
}
}

body.disable_ui [data-pluto-variable],
body.disable_ui [data-cell-variable] {
cursor: pointer;
Expand Down Expand Up @@ -3443,7 +3436,15 @@ pluto-cell.code_differs .cm-editor .cm-gutters {
}
pluto-input:focus-within .cm-editor .cm-lineNumbers .cm-gutterElement::after {
color: transparent;
} */
}
*/

pluto-cell.errored .cm-editor .cm-lineNumbers .cm-gutterElement {
color: var(--cm-line-numbers-color);
}
pluto-cell.errored .cm-editor .cm-lineNumbers .cm-gutterElement::after {
color: transparent;
}
/* Case 2: Print */
@media print {
.cm-editor .cm-lineNumbers .cm-gutterElement {
Expand Down
2 changes: 2 additions & 0 deletions frontend/light_color.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
--cm-line-numbers-color: #8d86875e;
--cm-selection-background: hsl(214deg 100% 73% / 48%);
--cm-selection-background-blurred: hsl(214deg 0% 73% / 48%);
--cm-highlighted: #cbceb668;

/* code highlighting */
--cm-editor-text-color: #41323f;
Expand All @@ -209,6 +210,7 @@
--cm-matchingBracket-color: black;
--cm-matchingBracket-bg-color: #1b4bbb21;
--cm-placeholder-text-color: rgba(0, 0, 0, 0.2);
--cm-clickable-underline: #ced2ef;

/*autocomplete menu*/
--autocomplete-menu-bg-color: white;
Expand Down
Loading
Loading