Fix diagram navigation and token highlighting

This commit is contained in:
alejo committed 2026-10-07 23:09:15 -03:00
1 parent 7ed885f0d1
commit 7aa729d41e
6 files changed
+354 -63

No files matched your search

+87 -14
View File
@@ -94,6 +94,18 @@ function cpMap(text) {
return function (k) { return k < map.length ? map[k] : text.length; };
}
// UTF-16 index -> code point index (the inverse of cpMap)
function unitToCp(text, u) {
if (!/[\ud800-\udbff]/.test(text)) return u;
var n = 0;
for (var i = 0; i < u && i < text.length; i++) {
var c = text.charCodeAt(i);
if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) i++;
n++;
}
return n;
}
/* ----------------------------------------------------------- line helpers */
function lineStarts(ed) {
if (!ed.ls) {
@@ -238,18 +250,23 @@ function lineSpan(t, a, b) {
return [s, e];
}
/* ------------------------------------------------------------------ indent
* Indentation is made of hard tabs (the editor shows a tab as 4 columns). A line
* that is already indented with spaces keeps getting spaces, so one line never
* mixes the two. */
function indentFor(lead) { return lead.charAt(0) === ' ' ? ' ' : '\t'; }
function indentSelection(ed, outdent) {
var ta = ed.ta, t = ta.value, a = ta.selectionStart, b = ta.selectionEnd;
var unit = ed.tab.kind === 'grammar' ? ' ' : '\t';
if (a === b && !outdent) {
if (unit === '\t') { replaceRange(ed, a, b, '\t'); return; }
var col = a - (t.lastIndexOf('\n', a - 1) + 1);
replaceRange(ed, a, b, unit.slice(0, 4 - (col % 4)));
var head = t.slice(t.lastIndexOf('\n', a - 1) + 1, a);
if (/^ +$/.test(head)) replaceRange(ed, a, b, ' '.slice(0, 4 - (head.length % 4))); // inside space indentation
else replaceRange(ed, a, b, '\t');
return;
}
var sp = lineSpan(t, a, b);
var lines = t.slice(sp[0], sp[1]).split('\n').map(function (l) {
if (!outdent) return l.length ? unit + l : l;
if (!outdent) return l.length ? indentFor(l) + l : l;
if (l.charAt(0) === '\t') return l.slice(1);
var m = /^ {1,4}/.exec(l);
return m ? l.slice(m[0].length) : l;
@@ -259,6 +276,68 @@ function indentSelection(ed, outdent) {
ed.ta.setSelectionRange(sp[0], sp[0] + out.length);
}
// Enter: new line with the right indentation.
// - keeps the indentation of the current line, and one level more after an
// opening ( [ { (and, in grammars, after a rule name or a ':')
// - Enter between a pair like {|} opens a block: the closer moves to its own line
// - after the ';' that ends a rule (grammars) the next line starts at column 0
// - a line that holds only whitespace is emptied, so no trailing whitespace is left
function newline(ed) {
var ta = ed.ta, t = ta.value, a = ta.selectionStart, b = ta.selectionEnd;
var grammar = ed.tab.kind === 'grammar';
var ls = t.lastIndexOf('\n', a - 1) + 1;
var head = t.slice(ls, a);
var le = t.indexOf('\n', b);
if (le < 0) le = t.length;
var tail = t.slice(b, le);
var lead = /^[ \t]*/.exec(head)[0];
var code = head.replace(/\s+$/, '');
var unit = indentFor(lead);
var from = a, next = lead, caretAt = -1, to = b;
if (head.length === lead.length) {
from = ls; // whitespace-only so far: drop it from the line above
} else {
var last = ls + code.length - 1; // index of the last character of code
var cls = (grammar && t.length <= 400000) ? g4Classes(t) : null;
var kind = cls ? cls[last] : 0; // 1 comment, 2 string, 3 [set], 4 action, 10 operator, ...
var quiet = kind === 1 || kind === 2; // inside a comment or a string: only keep the indentation
if (!quiet && /[\[({]$/.test(code)) {
next = lead + unit;
var closer = /^[ \t]*[\])}]/.exec(tail);
if (closer) { // {|} -> { / indented / }
to = b + /^[ \t]*/.exec(tail)[0].length;
caretAt = from + 1 + next.length;
replaceRange(ed, from, to, '\n' + next + '\n' + lead);
ta.setSelectionRange(caretAt, caretAt);
return;
}
} else if (!quiet && grammar && kind === 10 && /;$/.test(code)) {
next = ''; // the rule is finished
} else if (!quiet && grammar && kind === 10 && /:$/.test(code)) {
next = lead + unit; // alternatives start after a ':'
} else if (!quiet && grammar && lead === '' && kind !== 4 &&
/^(?:fragment[ \t]+)?[A-Za-z_]\w*(?![\w.])/.test(code) &&
!/^(?:grammar|lexer|parser|import|options|tokens|channels|mode)\b/.test(code) &&
!/[;:{(]$/.test(code)) {
next = unit; // a rule name (or the start of a rule): its body is indented
}
}
replaceRange(ed, from, to, '\n' + next);
}
// Typing a closing bracket as the first thing on an indented line steps it back one level.
function electricCloser(ed, ev) {
var ta = ed.ta, a = ta.selectionStart;
if (a !== ta.selectionEnd) return false;
var t = ta.value, ls = t.lastIndexOf('\n', a - 1) + 1, head = t.slice(ls, a);
if (!head.length || !/^[ \t]+$/.test(head)) return false;
var keep = head.charAt(head.length - 1) === '\t' ? head.length - 1 : head.length - (((head.length - 1) % 4) + 1);
ev.preventDefault();
replaceRange(ed, ls, a, head.slice(0, keep) + ev.key);
return true;
}
function toggleComment(ed) {
if (ed.tab.kind !== 'grammar') return;
var ta = ed.ta, t = ta.value, sp = lineSpan(t, ta.selectionStart, ta.selectionEnd);
@@ -277,15 +356,8 @@ function toggleComment(ed) {
function editorKeys(ed, ev) {
var ta = ed.ta, k = ev.key, mod = ev.ctrlKey || ev.metaKey;
if (k === 'Tab' && !mod && !ev.altKey) { ev.preventDefault(); indentSelection(ed, ev.shiftKey); return; }
if (k === 'Enter' && !mod && !ev.altKey && !ev.shiftKey) {
ev.preventDefault();
var a = ta.selectionStart, t = ta.value, ls = t.lastIndexOf('\n', a - 1) + 1;
var lead = /^[ \t]*/.exec(t.slice(ls, a))[0];
var before = t.slice(ls, a).replace(/\s+$/, '');
var extra = (ed.tab.kind === 'grammar' && /[:({]$/.test(before)) ? ' ' : '';
replaceRange(ed, a, ta.selectionEnd, '\n' + lead + extra);
return;
}
if (k === 'Enter' && !mod && !ev.altKey && !ev.shiftKey) { ev.preventDefault(); newline(ed); return; }
if ((k === '}' || k === ')' || k === ']') && !mod && !ev.altKey && electricCloser(ed, ev)) return;
if (mod && k === '/') { ev.preventDefault(); toggleComment(ed); return; }
if (mod && (k === 'f' || k === 'F' || k === 'h' || k === 'H')) { ev.preventDefault(); openFind(ed); return; }
if (k === 'Escape' && ed.findBar && !ed.findBar.hidden) { ev.preventDefault(); closeFind(ed); }
@@ -455,6 +527,7 @@ function makeEditor(tab) {
ta.addEventListener(name, function (ev) {
if (name === 'keyup' && /^(Arrow|Page|Home|End)/.test(ev.key)) revealUnit(ed, ta.selectionEnd);
if (ed.onCaret) ed.onCaret(ed);
if (name === 'mouseup' && ev.button === 0 && ed.onPick) ed.onPick(ed); // a click: show it in the diagram
});
});
return ed;