550 lines
21 KiB
JavaScript
550 lines
21 KiB
JavaScript
|
|
/* ================================================================== editor
|
|
* A plain <textarea> sits on top of a <pre> that paints the same text with
|
|
* syntax colors and marks. Both have identical font metrics and size, so the
|
|
* textarea provides the caret, selection, IME and native undo while the <pre>
|
|
* provides the colors. One editor (and so one undo history) per tab. */
|
|
|
|
var CLS = ['', 't-com', 't-str', 't-set', 't-act', 't-ann', 't-kw', 't-tok', 't-def', 't-lab', 't-op', 't-tokdef'];
|
|
var MKCLS = [[1, 'mk-err'], [2, 'mk-warn'], [4, 'mk-hl'], [8, 'mk-find'], [16, 'mk-cur']];
|
|
var KW = { grammar: 1, lexer: 1, parser: 1, fragment: 1, options: 1, tokens: 1, channels: 1, 'import': 1, mode: 1, returns: 1,
|
|
locals: 1, throws: 1, 'catch': 1, 'finally': 1, skip: 1, more: 1, pushMode: 1, popMode: 1, type: 1, channel: 1 };
|
|
|
|
function isIdStart(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'; }
|
|
function isIdChar(c) { return isIdStart(c) || (c >= '0' && c <= '9'); }
|
|
|
|
// One class id per UTF-16 unit, for ANTLR grammar syntax.
|
|
function g4Classes(t) {
|
|
var n = t.length, cls = new Uint8Array(n), i = 0, prevKw = '';
|
|
function paint(a, b, c) { for (var k = a; k < b; k++) cls[k] = c; }
|
|
function quoted(from, close) {
|
|
var j = from + 1;
|
|
while (j < n && t.charAt(j) !== '\n') {
|
|
if (t.charAt(j) === '\\') { j += 2; continue; }
|
|
if (t.charAt(j) === close) { j++; break; }
|
|
j++;
|
|
}
|
|
return Math.min(j, n);
|
|
}
|
|
while (i < n) {
|
|
var c = t.charAt(i), j;
|
|
if (c === '/' && t.charAt(i + 1) === '/') {
|
|
j = t.indexOf('\n', i); if (j < 0) j = n;
|
|
paint(i, j, 1); i = j;
|
|
} else if (c === '/' && t.charAt(i + 1) === '*') {
|
|
j = t.indexOf('*/', i + 2); j = j < 0 ? n : j + 2;
|
|
paint(i, j, 1); i = j;
|
|
} else if (c === "'") {
|
|
j = quoted(i, "'"); paint(i, j, 2); i = j;
|
|
} else if (c === '[') {
|
|
j = quoted(i, ']'); paint(i, j, 3); i = j;
|
|
} else if (c === '{') {
|
|
var depth = 0; j = i;
|
|
while (j < n) {
|
|
var d = t.charAt(j);
|
|
if (d === "'" || d === '"') { j = quoted(j, d); continue; }
|
|
if (d === '{') depth++;
|
|
else if (d === '}' && --depth === 0) { j++; break; }
|
|
j++;
|
|
}
|
|
paint(i, j, 4); i = j;
|
|
} else if (c === '@' || c === '#') {
|
|
j = i + 1;
|
|
while (j < n && (t.charAt(j) === ' ' || t.charAt(j) === '\t') && c === '#') j++;
|
|
while (j < n && (isIdChar(t.charAt(j)) || t.charAt(j) === ':')) j++;
|
|
paint(i, j, 5); i = j;
|
|
} else if (isIdStart(c)) {
|
|
j = i + 1;
|
|
while (j < n && isIdChar(t.charAt(j))) j++;
|
|
var word = t.slice(i, j);
|
|
var k = j;
|
|
while (k < n && (t.charAt(k) === ' ' || t.charAt(k) === '\t' || t.charAt(k) === '\n' || t.charAt(k) === '\r')) k++;
|
|
var next = t.charAt(k), after = t.charAt(k + 1);
|
|
var ls = t.lastIndexOf('\n', i - 1) + 1;
|
|
var first = /^[ \t]*$/.test(t.slice(ls, i));
|
|
var def = next === ':' && after !== ':' && (first || prevKw === 'fragment');
|
|
var c0 = word.charAt(0);
|
|
var up = c0 >= 'A' && c0 <= 'Z';
|
|
var id;
|
|
if (KW[word] === 1 && !def && next !== ':') id = 6;
|
|
else if (def) id = up ? 11 : 8;
|
|
else if ((next === '=' && after !== '=') || (next === '+' && after === '=')) id = 9;
|
|
else id = up ? 7 : 0;
|
|
paint(i, j, id);
|
|
prevKw = word;
|
|
i = j;
|
|
} else {
|
|
if ('|:;()*+?~.,<>=-'.indexOf(c) >= 0) cls[i] = 10;
|
|
i++;
|
|
}
|
|
}
|
|
return cls;
|
|
}
|
|
|
|
// code point index -> UTF-16 index
|
|
function cpMap(text) {
|
|
if (!/[\ud800-\udbff]/.test(text)) return function (i) { return i; };
|
|
var map = [];
|
|
for (var i = 0; i < text.length;) {
|
|
map.push(i);
|
|
var c = text.charCodeAt(i);
|
|
i += (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) ? 2 : 1;
|
|
}
|
|
map.push(text.length);
|
|
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) {
|
|
var t = ed.ta.value, a = [0];
|
|
for (var i = t.indexOf('\n'); i >= 0; i = t.indexOf('\n', i + 1)) a.push(i + 1);
|
|
ed.ls = a;
|
|
}
|
|
return ed.ls;
|
|
}
|
|
function lineOf(ed, idx) {
|
|
var a = lineStarts(ed), lo = 0, hi = a.length - 1;
|
|
while (lo < hi) {
|
|
var mid = (lo + hi + 1) >> 1;
|
|
if (a[mid] <= idx) lo = mid; else hi = mid - 1;
|
|
}
|
|
return lo;
|
|
}
|
|
// 1-based line + 0-based column -> unit index (column clamped to the line)
|
|
function unitAt(ed, line, col) {
|
|
var a = lineStarts(ed);
|
|
if (line < 1) return 0;
|
|
if (line > a.length) return ed.ta.value.length;
|
|
var s = a[line - 1], e = line < a.length ? a[line] - 1 : ed.ta.value.length;
|
|
return Math.min(s + col, e);
|
|
}
|
|
|
|
var metricCache = {};
|
|
function metrics(ed) {
|
|
var key = S.prefs.fs;
|
|
if (metricCache[key]) return metricCache[key];
|
|
var probe = el('span', '', '0000000000000000000000000000000000000000');
|
|
probe.style.position = 'absolute';
|
|
probe.style.visibility = 'hidden';
|
|
probe.style.whiteSpace = 'pre';
|
|
probe.className = 'hl';
|
|
probe.style.padding = '0';
|
|
probe.style.minHeight = '0';
|
|
ed.hl.parentNode.appendChild(probe);
|
|
var w = probe.getBoundingClientRect().width / 40;
|
|
probe.remove();
|
|
var lh = parseFloat(getComputedStyle(ed.ta).lineHeight) || 20;
|
|
metricCache[key] = { cw: w || 8, lh: lh };
|
|
return metricCache[key];
|
|
}
|
|
|
|
// Scrolls the editor so that the given unit index is visible.
|
|
function revealUnit(ed, idx) {
|
|
var m = metrics(ed), sc = ed.scroller;
|
|
var line = lineOf(ed, idx);
|
|
var ls = lineStarts(ed)[line];
|
|
var col = 0, pre = ed.ta.value.slice(ls, idx);
|
|
for (var i = 0; i < pre.length; i++) col = pre.charAt(i) === '\t' ? (Math.floor(col / 4) + 1) * 4 : col + 1;
|
|
var y = 8 + line * m.lh;
|
|
var gw = ed.gutter.offsetWidth;
|
|
var x = gw + 10 + col * m.cw;
|
|
if (y < sc.scrollTop + m.lh) sc.scrollTop = Math.max(0, y - m.lh * 2);
|
|
else if (y + m.lh * 2 > sc.scrollTop + sc.clientHeight) sc.scrollTop = y + m.lh * 3 - sc.clientHeight;
|
|
if (x < sc.scrollLeft + gw + 30) sc.scrollLeft = Math.max(0, x - gw - 60);
|
|
else if (x + 40 > sc.scrollLeft + sc.clientWidth) sc.scrollLeft = x + 80 - sc.clientWidth;
|
|
}
|
|
|
|
function caretInfo(ed) {
|
|
var p = ed.ta.selectionEnd, line = lineOf(ed, p);
|
|
return { line: line + 1, col: p - lineStarts(ed)[line] + 1 };
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- painting */
|
|
function paintEditor(ed) {
|
|
var t = ed.ta.value, n = t.length;
|
|
var big = n > 800000;
|
|
var cls = (ed.tab.kind === 'grammar' && !big) ? g4Classes(t) : null;
|
|
var mk = new Uint8Array(n + 1);
|
|
function mark(list, bit) {
|
|
for (var i = 0; i < list.length; i++) {
|
|
var r = list[i];
|
|
for (var k = Math.max(0, r.s); k < Math.min(n, r.e); k++) mk[k] |= (r.bit || bit);
|
|
}
|
|
}
|
|
if (!big) {
|
|
mark(ed.diag, 1);
|
|
if (ed.find) {
|
|
for (var f = 0; f < ed.find.matches.length && f < 5000; f++) {
|
|
var fm = ed.find.matches[f];
|
|
for (var q = fm[0]; q < fm[1]; q++) mk[q] |= (f === ed.find.cur ? 16 : 8);
|
|
}
|
|
}
|
|
var rng = ed.hover || ed.pin;
|
|
if (rng) for (var h = Math.max(0, rng[0]); h < Math.min(n, rng[1]); h++) mk[h] |= 4;
|
|
}
|
|
|
|
var frag = document.createDocumentFragment();
|
|
var i = 0;
|
|
while (i < n) {
|
|
var key = (cls ? cls[i] : 0) | (mk[i] << 4), j = i + 1;
|
|
while (j < n && ((cls ? cls[j] : 0) | (mk[j] << 4)) === key) j++;
|
|
var txt = t.slice(i, j);
|
|
if (key === 0) {
|
|
frag.appendChild(document.createTextNode(txt));
|
|
} else {
|
|
var names = [];
|
|
if (key & 15) names.push(CLS[key & 15]);
|
|
for (var m = 0; m < MKCLS.length; m++) if ((key >> 4) & MKCLS[m][0]) names.push(MKCLS[m][1]);
|
|
frag.appendChild(el('span', names.join(' '), txt));
|
|
}
|
|
i = j;
|
|
}
|
|
frag.appendChild(document.createTextNode('\n')); // matches the textarea's trailing line
|
|
clear(ed.hl);
|
|
ed.hl.appendChild(frag);
|
|
|
|
var lines = lineStarts(ed).length;
|
|
if (lines !== ed.lines) {
|
|
var nums = [];
|
|
for (var L = 1; L <= lines; L++) nums.push(L);
|
|
ed.gutter.textContent = nums.join('\n') + '\n';
|
|
ed.lines = lines;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- editing */
|
|
function replaceRange(ed, a, b, text) {
|
|
var ta = ed.ta;
|
|
ta.focus();
|
|
ta.setSelectionRange(a, b);
|
|
var ok = false;
|
|
// execCommand keeps the undo history but is very slow for big multi-line
|
|
// strings in Chrome, so large edits take the direct route.
|
|
if (text.length + (b - a) <= 20000) {
|
|
try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }
|
|
}
|
|
if (!ok) {
|
|
ta.setRangeText(text, a, b, 'end');
|
|
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
function lineSpan(t, a, b) {
|
|
if (b > a && t.charAt(b - 1) === '\n') b--;
|
|
var s = t.lastIndexOf('\n', a - 1) + 1;
|
|
var e = t.indexOf('\n', b);
|
|
if (e < 0) e = t.length;
|
|
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;
|
|
if (a === b && !outdent) {
|
|
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 ? 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;
|
|
});
|
|
var out = lines.join('\n');
|
|
replaceRange(ed, sp[0], sp[1], out);
|
|
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);
|
|
var lines = t.slice(sp[0], sp[1]).split('\n');
|
|
var live = lines.filter(function (l) { return l.trim().length; });
|
|
var all = live.length && live.every(function (l) { return /^\s*\/\//.test(l); });
|
|
var indent = Math.min.apply(null, live.map(function (l) { return /^\s*/.exec(l)[0].length; }).concat([1e9]));
|
|
var out = lines.map(function (l) {
|
|
if (!l.trim().length) return l;
|
|
return all ? l.replace(/^(\s*)\/\/ ?/, '$1') : l.slice(0, indent) + '// ' + l.slice(indent);
|
|
}).join('\n');
|
|
replaceRange(ed, sp[0], sp[1], out);
|
|
ta.setSelectionRange(sp[0], sp[0] + out.length);
|
|
}
|
|
|
|
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(); 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); }
|
|
}
|
|
|
|
/* -------------------------------------------------------------------- find */
|
|
function buildFind(ed) {
|
|
var bar = el('div', 'find');
|
|
bar.setAttribute('role', 'search');
|
|
var r1 = el('div', 'row'), r2 = el('div', 'row');
|
|
var q = el('input'); q.type = 'text'; q.placeholder = 'Find'; q.setAttribute('aria-label', 'Find');
|
|
var cnt = el('span', 'cnt');
|
|
var bPrev = el('button', 'btn sm', '\u2191'); bPrev.title = 'Previous (Shift+Enter)';
|
|
var bNext = el('button', 'btn sm', '\u2193'); bNext.title = 'Next (Enter)';
|
|
var bCase = el('button', 'btn sm', 'Aa'); bCase.title = 'Match case'; bCase.setAttribute('aria-pressed', 'false');
|
|
var bX = el('button', 'btn sm', '\u00d7'); bX.title = 'Close (Esc)';
|
|
var rep = el('input'); rep.type = 'text'; rep.placeholder = 'Replace'; rep.setAttribute('aria-label', 'Replace with');
|
|
var bRep = el('button', 'btn sm', 'Replace');
|
|
var bAll = el('button', 'btn sm', 'All');
|
|
[bPrev, bNext, bCase, bX, bRep, bAll].forEach(function (b) { b.type = 'button'; });
|
|
[q, bPrev, bNext, bCase, cnt, bX].forEach(function (n) { r1.appendChild(n); });
|
|
[rep, bRep, bAll].forEach(function (n) { r2.appendChild(n); });
|
|
bar.appendChild(r1); bar.appendChild(r2);
|
|
ed.root.appendChild(bar);
|
|
ed.findBar = bar;
|
|
ed.fq = q; ed.fr = rep; ed.fcnt = cnt; ed.fcase = false;
|
|
|
|
q.oninput = function () { runFind(ed, true); };
|
|
q.onkeydown = function (ev) {
|
|
if (ev.key === 'Enter') { ev.preventDefault(); stepFind(ed, ev.shiftKey ? -1 : 1); }
|
|
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
|
|
};
|
|
rep.onkeydown = function (ev) {
|
|
if (ev.key === 'Enter') { ev.preventDefault(); replaceOne(ed); }
|
|
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
|
|
};
|
|
bPrev.onclick = function () { stepFind(ed, -1); };
|
|
bNext.onclick = function () { stepFind(ed, 1); };
|
|
bCase.onclick = function () {
|
|
ed.fcase = !ed.fcase;
|
|
bCase.setAttribute('aria-pressed', ed.fcase ? 'true' : 'false');
|
|
runFind(ed, true);
|
|
};
|
|
bX.onclick = function () { closeFind(ed); };
|
|
bRep.onclick = function () { replaceOne(ed); };
|
|
bAll.onclick = function () { replaceAll(ed); };
|
|
}
|
|
|
|
function openFind(ed) {
|
|
if (!ed.findBar) buildFind(ed);
|
|
ed.findBar.hidden = false;
|
|
var sel = ed.ta.value.slice(ed.ta.selectionStart, ed.ta.selectionEnd);
|
|
if (sel && sel.indexOf('\n') < 0) ed.fq.value = sel;
|
|
ed.fq.focus();
|
|
ed.fq.select();
|
|
runFind(ed, false);
|
|
}
|
|
|
|
function closeFind(ed) {
|
|
if (!ed.findBar) return;
|
|
ed.findBar.hidden = true;
|
|
ed.find = null;
|
|
paintEditor(ed);
|
|
ed.ta.focus();
|
|
}
|
|
|
|
function findMatches(ed) {
|
|
var q = ed.fq.value, out = [];
|
|
if (!q) return out;
|
|
var hay = ed.fcase ? ed.ta.value : ed.ta.value.toLowerCase();
|
|
var needle = ed.fcase ? q : q.toLowerCase();
|
|
for (var i = hay.indexOf(needle); i >= 0 && out.length < 5000; i = hay.indexOf(needle, i + Math.max(1, needle.length))) {
|
|
out.push([i, i + needle.length]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function runFind(ed, jump) {
|
|
var ms = findMatches(ed), cur = -1;
|
|
if (ms.length) {
|
|
var from = ed.ta.selectionStart;
|
|
for (var i = 0; i < ms.length; i++) { if (ms[i][0] >= from) { cur = i; break; } }
|
|
if (cur < 0) cur = 0;
|
|
}
|
|
ed.find = ms.length || ed.fq.value ? { matches: ms, cur: cur } : null;
|
|
ed.fcnt.textContent = ed.fq.value ? (ms.length ? (cur + 1) + ' of ' + ms.length : 'No results') : '';
|
|
paintEditor(ed);
|
|
if (jump && cur >= 0) selectMatch(ed, ms[cur]);
|
|
}
|
|
|
|
function selectMatch(ed, m) {
|
|
ed.ta.setSelectionRange(m[0], m[1]);
|
|
revealUnit(ed, m[0]);
|
|
}
|
|
|
|
function stepFind(ed, dir) {
|
|
if (!ed.find || !ed.find.matches.length) { runFind(ed, true); return; }
|
|
var n = ed.find.matches.length;
|
|
ed.find.cur = (ed.find.cur + dir + n) % n;
|
|
ed.fcnt.textContent = (ed.find.cur + 1) + ' of ' + n;
|
|
paintEditor(ed);
|
|
selectMatch(ed, ed.find.matches[ed.find.cur]);
|
|
}
|
|
|
|
function replaceOne(ed) {
|
|
if (!ed.find || ed.find.cur < 0) { runFind(ed, true); return; }
|
|
var m = ed.find.matches[ed.find.cur];
|
|
replaceRange(ed, m[0], m[1], ed.fr.value);
|
|
ed.fq.focus();
|
|
runFind(ed, true);
|
|
}
|
|
|
|
function replaceAll(ed) {
|
|
var ms = findMatches(ed);
|
|
if (!ms.length) return;
|
|
var t = ed.ta.value, out = '', last = 0;
|
|
ms.forEach(function (m) { out += t.slice(last, m[0]) + ed.fr.value; last = m[1]; });
|
|
out += t.slice(last);
|
|
replaceRange(ed, 0, t.length, out);
|
|
ed.fq.focus();
|
|
runFind(ed, false);
|
|
toast('Replaced ' + plural(ms.length, 'match'));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ editor */
|
|
function makeEditor(tab) {
|
|
var root = el('div', 'ed');
|
|
root.hidden = true;
|
|
var banner = el('div', 'banner');
|
|
banner.hidden = true;
|
|
var scroller = el('div', 'scroller');
|
|
var rows = el('div', 'rows');
|
|
var gutter = el('pre', 'gutter');
|
|
gutter.setAttribute('aria-hidden', 'true');
|
|
var content = el('div', 'content');
|
|
var hl = el('pre', 'hl');
|
|
hl.setAttribute('aria-hidden', 'true');
|
|
var ta = el('textarea', 'ta');
|
|
ta.spellcheck = false;
|
|
ta.setAttribute('wrap', 'off');
|
|
ta.setAttribute('autocomplete', 'off');
|
|
ta.setAttribute('autocorrect', 'off');
|
|
ta.setAttribute('autocapitalize', 'off');
|
|
ta.setAttribute('aria-label', 'Editor');
|
|
content.appendChild(hl);
|
|
content.appendChild(ta);
|
|
rows.appendChild(gutter);
|
|
rows.appendChild(content);
|
|
scroller.appendChild(rows);
|
|
root.appendChild(banner);
|
|
root.appendChild(scroller);
|
|
|
|
var ed = { tab: tab, root: root, banner: banner, scroller: scroller, gutter: gutter, hl: hl, ta: ta,
|
|
diag: [], hover: null, pin: null, find: null, ls: null, lines: 0, findBar: null, onInput: null, onCaret: null };
|
|
|
|
ta.addEventListener('input', function () {
|
|
ed.ls = null;
|
|
ed.diag = [];
|
|
ed.pin = null;
|
|
if (ed.find) runFind(ed, false); else paintEditor(ed);
|
|
revealUnit(ed, ta.selectionEnd);
|
|
if (ed.onInput) ed.onInput(ed);
|
|
if (ed.onCaret) ed.onCaret(ed);
|
|
});
|
|
ta.addEventListener('keydown', function (ev) { editorKeys(ed, ev); });
|
|
['keyup', 'mouseup', 'focus'].forEach(function (name) {
|
|
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;
|
|
}
|
|
|
|
function setEditorText(ed, text) {
|
|
ed.ta.value = text;
|
|
ed.ls = null;
|
|
ed.diag = [];
|
|
ed.pin = null;
|
|
ed.find = null;
|
|
if (ed.findBar) { ed.findBar.hidden = true; }
|
|
paintEditor(ed);
|
|
}
|
|
|
|
function setEditorRange(ed, kind, range) { // kind: 'hover' | 'pin'; range [s,e) in units or null
|
|
ed[kind] = range;
|
|
paintEditor(ed);
|
|
}
|