Skip to content

Commit 8082d15

Browse files
committed
frontend: Highlight lines with error
Highlight lines with evy errors in editor. This is done by finding the correct token by its line and columns and adding the "err" CSS class to it. It is becoming increasingly obvious that a more fully fledged web code editor will be needed. Evy files with more than 700ish lines are starting to get janky when editing. I could work around this by only updating the relevant lines, but it all seems to get a bit to hacky. After this error highlight addition we will leave editor hacking for now.
1 parent b5c53d3 commit 8082d15

File tree

6 files changed

+149
-58
lines changed

6 files changed

+149
-58
lines changed

frontend/img/err-line.svg

Lines changed: 3 additions & 0 deletions
Loading

frontend/index.css

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
--output-border-dark: hsl(0deg 0% 80%);
1212
--output-border-mid: hsl(0deg 0% 85%);
1313
--output-border-light: hsl(0deg 0% 92%);
14+
--line-num-color: hsl(212deg 8% 47%);
1415
--syntax-num: hsl(204deg 100% 75%);
1516
--syntax-string: hsl(203deg 100% 86%);
1617
--syntax-func: hsl(266deg 100% 86%);
1718
--syntax-builtin: hsl(27deg 100% 74%);
1819
--syntax-keyword: hsl(359deg 100% 75%);
1920
--syntax-comment: hsl(210deg 13% 72%);
21+
--syntax-error-line-bg: hsl(210deg 7% 20%);
2022
--syntax-error-bg: hsl(209deg 100% 33%);
2123
--btn-color: hsl(0deg 0% 33%);
2224
--btn-color-active: hsl(0deg 0% 100%);
@@ -223,7 +225,6 @@ main.view-output {
223225
/* --- Editor -------------------------------------------------------- */
224226
.editor-wrap {
225227
margin-top: var(--editor-padding);
226-
padding-right: var(--editor-padding);
227228
padding-bottom: var(--editor-padding-bottom);
228229
font-size: 1rem;
229230
flex: 1;
@@ -239,6 +240,7 @@ main.view-output {
239240
position: relative;
240241
overflow: hidden;
241242
width: max-content;
243+
min-width: 100%;
242244
}
243245
.editor textarea {
244246
line-height: inherit;
@@ -259,12 +261,11 @@ main.view-output {
259261
border: none;
260262
top: 0;
261263
left: 0;
262-
color: transparent;
263264
overflow: hidden;
265+
color: transparent;
264266
}
265267
.editor pre {
266268
line-height: inherit;
267-
position: relative;
268269
white-space: pre-wrap;
269270
word-break: keep-all;
270271
padding: 0;
@@ -275,6 +276,9 @@ main.view-output {
275276
letter-spacing: inherit;
276277
pointer-events: none;
277278
font-family: inherit;
279+
}
280+
.editor pre.highlighted {
281+
position: relative;
278282
overflow: hidden;
279283
}
280284
.editor pre.lines {
@@ -284,6 +288,11 @@ main.view-output {
284288
left: 0;
285289
pointer-events: none;
286290
overflow: auto;
291+
min-width: 100%;
292+
}
293+
.editor pre.highlighted .err {
294+
background: var(--syntax-error-bg);
295+
border-radius: 6px;
287296
}
288297
.editor .num,
289298
.editor .bool {
@@ -316,14 +325,35 @@ main.view-output {
316325
}
317326
.editor .lines .num {
318327
position: absolute;
319-
color: hsl(212deg 8% 47%);
328+
color: var(--line-num-color);
320329
left: 0;
321330
}
322331
.editor .lines .txt {
323-
padding-left: 1ch;
324332
color: transparent;
325333
pointer-events: none;
326334
}
335+
.editor .lines .err.num {
336+
background: var(--syntax-error-bg);
337+
color: var(--color);
338+
}
339+
.editor .lines .err.txt {
340+
left: calc(2ch + 1.2rem);
341+
right: 0;
342+
padding-left: 0.3rem;
343+
background: var(--syntax-error-line-bg);
344+
border-radius: 6px;
345+
position: absolute;
346+
}
347+
.editor .lines .err.num::after {
348+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 10 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0 H2 L10 12 L2 24 H0V0 Z' fill='%230057A8'/%3E%3C/svg%3E%0A");
349+
background-repeat: no-repeat;
350+
content: "";
351+
width: 0.8rem;
352+
position: absolute;
353+
right: -0.8rem;
354+
top: 0;
355+
bottom: 0;
356+
}
327357

328358
/* --- Output -------------------------------------------------------- */
329359
.output {

frontend/index.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function newEvyGo() {
4141
jsRead,
4242
jsActions,
4343
jsPrepareUI,
44+
jsError,
4445
evySource,
4546
setEvySource,
4647
move,
@@ -108,6 +109,24 @@ function jsRead() {
108109
return stringToMemAddr(s.slice(0, idx))
109110
}
110111

112+
function jsError(ptr, len) {
113+
const code = editor.value
114+
const lines = code.split("\n")
115+
const errs = memToString(ptr, len).split("\n")
116+
const re = /line (?<line>\d+) column (?<col>\d+): (?<msg>.*)/
117+
let msgs = ""
118+
const errorLines = {}
119+
for (const err of errs) {
120+
const g = err.match(re).groups
121+
errorLines[g.line] = { col: g.col, text: lines[g.line - 1] }
122+
msgs += `line ${g.line}: ${g.msg}\n`
123+
}
124+
const output = document.querySelector("#console")
125+
output.textContent = msgs
126+
output.scrollTo({ behavior: "smooth", left: 0, top: 0 })
127+
editor.update({ errorLines })
128+
}
129+
111130
// evySource writes the evy source code into wasm memory as bytes
112131
// and returns pointer and length encoded into a single 64 bit number
113132
function evySource() {
@@ -118,7 +137,7 @@ function evySource() {
118137
// setEvySource is exported to evy go/wasm and called after formatting
119138
function setEvySource(ptr, len) {
120139
const source = memToString(ptr, len)
121-
editor.update({ value: source })
140+
editor.update({ value: source, errorLines: {} })
122141
}
123142

124143
function memToString(ptr, len) {
@@ -301,7 +320,7 @@ async function handleHashChange() {
301320
throw new Error("invalid response status", response.status)
302321
}
303322
const source = await response.text()
304-
editor.update({ value: source })
323+
editor.update({ value: source, errorLines: {} })
305324
document.querySelector(".editor-wrap").scrollTo(0, 0)
306325
updateBreadcrumbs(crumbs)
307326
clearOutput()

frontend/module/yace-editor.js

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ export default class Yace {
3636
this.textarea.autocapitalize = false
3737
this.textarea.wrap = "off"
3838

39-
this.pre = document.createElement("pre")
39+
this.highlighted = document.createElement("pre")
40+
this.highlighted.classList.add("highlighted")
41+
this.lines = document.createElement("pre")
42+
this.lines.classList.add("lines")
43+
this.errorLines = {}
4044

4145
this.root.appendChild(this.textarea)
42-
this.root.appendChild(this.pre)
46+
this.root.appendChild(this.lines)
47+
this.root.appendChild(this.highlighted)
4348

4449
this.addTextareaEvents()
4550
this.update({ value: this.options.value })
46-
this.updateLines()
4751
}
4852

4953
addTextareaEvents() {
@@ -62,7 +66,7 @@ export default class Yace {
6266
}
6367

6468
update(textareaProps) {
65-
const { value, selectionStart, selectionEnd } = textareaProps
69+
const { value, selectionStart, selectionEnd, errorLines } = textareaProps
6670
// should be before updating selection otherwise selection will be lost
6771
if (value != null) {
6872
this.textarea.value = value
@@ -71,50 +75,52 @@ export default class Yace {
7175
this.textarea.selectionStart = selectionStart
7276
this.textarea.selectionEnd = selectionEnd
7377

74-
if (value === this.value || value == null) {
78+
if (
79+
(value === this.value || value == null) &&
80+
(!errorLines || Object.keys(errorLines).length === 0)
81+
) {
7582
return
7683
}
84+
this.value = value || this.value
85+
this.errorLines = errorLines || this.errorLines
86+
const lines = this.value.split("\n")
87+
this.updateErrorLines(lines)
88+
const highlighted = this.options.highlighter(this.value, this.errorLines)
89+
this.highlighted.innerHTML = highlighted + "<br/>"
7790

78-
this.value = value
79-
80-
const highlighted = this.options.highlighter(value)
81-
this.pre.innerHTML = highlighted + "<br/>"
82-
83-
this.updateLines()
91+
this.updateLines(lines)
8492

8593
if (this.updateCallback) {
8694
this.updateCallback(value)
8795
}
8896
}
8997

90-
updateLines() {
91-
if (!this.options.lineNumbers) {
92-
return
93-
}
94-
95-
if (!this.lines) {
96-
this.lines = document.createElement("pre")
97-
this.root.appendChild(this.lines)
98-
this.lines.classList.add("lines")
99-
}
100-
101-
const lines = this.value.split("\n")
98+
updateLines(lines) {
10299
const length = lines.length.toString().length
103100

104-
this.root.style.paddingLeft = `${length + 2}ch`
101+
const paddingLeft = `calc(${length}ch + 1.5rem)`
102+
this.root.style.paddingLeft = paddingLeft
103+
this.lines.style.paddingLeft = paddingLeft
105104

106105
this.lines.innerHTML = lines
107106
.map((line, number) => {
108-
// prettier-ignore
109-
const num = `${number+1}`.padStart(length)
110-
const lineNumber = `<span class="num"> ${num}</span>`
111-
// prettier-ignore
112-
const lineText = `<span class="txt">${escape(line)}</span>`;
107+
const num = `${number + 1}`.padStart(length)
108+
const errClass = this.errorLines[number + 1] ? "err " : ""
109+
const lineNumber = `<span class="${errClass}num"> ${num}</span>`
110+
const lineText = `<span class="${errClass}txt">${escape(line)}</span>`
113111
return `${lineNumber}${lineText}`
114112
})
115113
.join("\n")
116114
}
117115

116+
updateErrorLines(lines) {
117+
for (const [idx, { text }] of Object.entries(this.errorLines)) {
118+
if (lines[idx - 1] !== text) {
119+
delete this.errorLines[idx]
120+
}
121+
}
122+
}
123+
118124
destroy() {
119125
this.textarea.removeEventListener("input", this.handleInput)
120126
this.textarea.removeEventListener("keydown", this.handleKeydown)
@@ -427,11 +433,9 @@ const tab =
427433
}
428434
}
429435
// evy highlighter
430-
function highlightEvy(val) {
431-
const { tokens, funcs } = tokenize(val)
432-
const type = (t) => (t.type === "ident" && funcs.has(t.val) ? "func" : t.type)
433-
434-
const span = (t) => `<span class="${type(t)}">${escape(t.val)}</span>`
436+
function highlightEvy(val, errorLines) {
437+
const tokens = tokenize(val, errorLines)
438+
const span = (t) => `<span class="${t.err}${t.type}">${escape(t.val)}</span>`
435439
const result = tokens.map((t) => span(t)).join("")
436440
return result
437441
}
@@ -490,56 +494,83 @@ const keywords = new Set([
490494
"end",
491495
])
492496

493-
function tokenize(str) {
497+
function tokenize(str, errorLines) {
494498
let tokens = []
495499
let i = 0
496500
let prev = ""
497501
let funcs = new Set()
498-
while (i < str.length) {
502+
let lineIdx = 1
503+
let lineOffset = 0
504+
const chars = Array.from(str)
505+
506+
while (i < chars.length) {
499507
const start = i
500-
const c = str[i]
508+
const c = chars[i]
501509
let type
502510
i++
503511
if (isWS(c)) {
504512
type = "ws"
505-
i = readWS(str, i)
513+
i = readWS(chars, i)
506514
} else if (isOP(c)) {
507515
type = "op"
508-
str[i] === "=" && i++
509-
} else if (c === ":" && str[i] === "=") {
516+
chars[i] === "=" && i++
517+
} else if (c === ":" && chars[i] === "=") {
510518
i++
511519
type = "op"
512-
} else if (isPunc(c) || (c === ":" && str[i] !== "=")) {
520+
} else if (isPunc(c) || (c === ":" && chars[i] !== "=")) {
513521
type = "punc"
514-
} else if (c === "/" && str[i] == "/") {
522+
} else if (c === "/" && chars[i] == "/") {
515523
type = "comment"
516-
i = readComment(str, i)
517-
} else if (c === "/" && str[i] != "/") {
524+
i = readComment(chars, i)
525+
} else if (c === "/" && chars[i] != "/") {
518526
type = "op"
519527
} else if (c === '"') {
520528
type = "str"
521-
i = readString(str, i)
529+
i = readString(chars, i)
522530
} else if (isDigit(c)) {
523531
type = "num"
524-
i = readNum(str, i)
532+
i = readNum(chars, i)
525533
} else if (isLetter(c)) {
526534
type = "ident"
527-
i = readIdent(str, i)
535+
i = readIdent(chars, i)
528536
} else if (c === "\n") {
529537
type = "nl"
530538
} else {
531539
type = "error"
532540
}
533-
const val = str.substring(start, i)
541+
let val = chars.slice(start, i).join("")
534542
if (type == "ident") {
535543
type = identType(val, prev, funcs)
536544
}
537-
tokens.push({ type, val })
545+
const errLine = errorLines[lineIdx]
546+
let err = ""
547+
if (errLine) {
548+
const errCol = errLine.col - 1
549+
const startCol = start - lineOffset
550+
const endCol = i - lineOffset
551+
if (errCol >= startCol && errCol < endCol) {
552+
err = "err "
553+
}
554+
console.log("errline errCol", errCol, "start-end:", startCol, "-", endCol, val)
555+
}
538556
if (type !== "ws") {
539557
prev = val
540558
}
559+
if (type === "nl") {
560+
lineIdx++
561+
lineOffset = i
562+
if (err) {
563+
val = " \n"
564+
}
565+
}
566+
tokens.push({ type, val, err })
541567
}
542-
return { tokens, funcs }
568+
tokens.forEach((t) => {
569+
if (t.type === "ident" && funcs.has(t.val)) {
570+
t.type = "func"
571+
}
572+
})
573+
return tokens
543574
}
544575

545576
function isWS(s) {

pkg/wasm/imports.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ func jsRead() float64
114114
//export jsPrint
115115
func jsPrint(string)
116116

117+
// jsError is imported from JS. jsError is used for setting compile time
118+
// errors of format:
119+
//
120+
// line NUM column NUM: ERROR_DETAILS
121+
//
122+
//export jsError
123+
func jsError(string)
124+
117125
// afterStop is imported from JS
118126
//
119127
//export afterStop

0 commit comments

Comments
 (0)