/* ==================================================================== tabs */ var nextTabId = 1; var NEW_GRAMMAR = 'grammar MyGrammar;\n\nprog : expr EOF ;\n\nexpr : term (\'+\' term)* ;\n\nterm : INT | ID | \'(\' expr \')\' ;\n\nINT : [0-9]+ ;\nID : [a-zA-Z_] [a-zA-Z_0-9]* ;\nWS : [ \\t\\r\\n]+ -> skip ;\n'; function grammarName(text) { var m = /^\s*(?:lexer\s+|parser\s+)?grammar\s+([A-Za-z_]\w*)\s*;/m.exec(text.slice(0, 20000)); return m ? m[1] : ''; } function getTab(id) { for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].id === id) return S.tabs[i]; return null; } function tabTitle(t) { if (t.path) return basename(t.path); if (t.kind === 'grammar') { var n = grammarName(t.ed.ta.value); return (n || 'Untitled') + '.g4'; } return 'Input ' + t.num; } function firstOfKind(kind) { for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].kind === kind) return S.tabs[i]; return null; } function ensureLast() { var g = getTab(S.lastGrammar), i = getTab(S.lastInput); if (!g || g.kind !== 'grammar') { g = firstOfKind('grammar'); S.lastGrammar = g ? g.id : null; } if (!i || i.kind !== 'input') { i = firstOfKind('input'); S.lastInput = i ? i.id : null; } } function addTab(o) { var t = { id: 't' + (nextTabId++), kind: o.kind, path: o.path || '', num: 0, saved: o.text, etag: o.etag || '', eol: o.eol || 'lf', bom: !!o.bom, dirty: false, rev: 0, banner: '', diskEtag: '', shown: '' }; if (!t.path) t.num = ++S.untitled[t.kind]; t.ed = makeEditor(t); t.ed.onInput = function () { onEdit(t); }; t.ed.onCaret = updatePos; t.ed.onPick = function () { if (t.kind === 'input') focusDiagramAt(t); }; setEditorText(t.ed, o.text); $('editors').appendChild(t.ed.root); S.tabs.push(t); return t; } function onEdit(t) { t.rev++; if (t.kind === 'grammar') S.grev++; t.dirty = t.ed.ta.value !== t.saved; var sig = (t.dirty ? '*' : '') + tabTitle(t); if (sig !== t.shown) renderTabs(); refreshPanels(); if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(t.kind === 'grammar' ? 800 : 500); } function activate(id, focus) { var t = getTab(id); if (!t) return; S.active = id; if (t.kind === 'grammar') S.lastGrammar = id; else S.lastInput = id; ensureLast(); S.tabs.forEach(function (x) { x.ed.root.hidden = x.id !== id; }); $('welcome').hidden = true; renderTabs(); updateBanner(t); updatePos(); refreshPanels(); if (focus !== false) t.ed.ta.focus(); rememberSession(); if (S.prefs.autorun) { var it = getTab(S.lastInput), r = it ? S.results[it.id] : null; if (S.lastGrammar && it && (!r || r.rev !== it.rev || r.grev !== S.grev || r.main !== S.lastGrammar)) scheduleRun(120); } } function showEmpty() { S.active = null; $('welcome').hidden = false; renderTabs(); updatePos(); refreshPanels(); } function renderTabs() { var bar = $('tabs'); clear(bar); var activeEl = null; S.tabs.forEach(function (t) { var title = tabTitle(t); t.shown = (t.dirty ? '*' : '') + title; var on = t.id === S.active; var d = el('div', 'tab'); d.setAttribute('role', 'tab'); d.setAttribute('aria-selected', on ? 'true' : 'false'); d.tabIndex = on ? 0 : -1; d.title = t.path || 'Not saved to disk yet'; if (t.dirty) { var dot = el('span', 'dot'); dot.title = 'Unsaved changes'; d.appendChild(dot); } d.appendChild(el('span', '', title)); var kind = el('button', 'kind', t.kind === 'grammar' ? 'G' : 'I'); kind.type = 'button'; kind.title = (t.kind === 'grammar' ? 'Grammar' : 'Input') + ' - click to treat this file as ' + (t.kind === 'grammar' ? 'an input' : 'a grammar'); kind.onclick = function (ev) { ev.stopPropagation(); toggleKind(t); }; d.appendChild(kind); var x = el('button', 'tx', '\u00d7'); x.type = 'button'; x.setAttribute('aria-label', 'Close ' + title); x.onclick = function (ev) { ev.stopPropagation(); closeTab(t.id); }; d.appendChild(x); d.onclick = function () { activate(t.id); }; d.onauxclick = function (ev) { if (ev.button === 1) { ev.preventDefault(); closeTab(t.id); } }; d.onkeydown = function (ev) { var i = S.tabs.indexOf(t), n = S.tabs.length; if (ev.key === 'ArrowRight') { ev.preventDefault(); activate(S.tabs[(i + 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); } else if (ev.key === 'ArrowLeft') { ev.preventDefault(); activate(S.tabs[(i + n - 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); } else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); activate(t.id); } }; bar.appendChild(d); if (on) activeEl = d; }); if (activeEl && activeEl.scrollIntoView) activeEl.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } function toggleKind(t) { t.kind = t.kind === 'grammar' ? 'input' : 'grammar'; delete S.results[t.id]; S.grev++; ensureLast(); paintEditor(t.ed); if (t.id === S.active) activate(t.id, false); else renderTabs(); refreshPanels(); scheduleRun(200); } function closeTab(id, force) { var t = getTab(id); if (!t) return Promise.resolve(true); function proceed() { var idx = S.tabs.indexOf(t); S.tabs.splice(idx, 1); t.ed.root.remove(); delete S.results[id]; if (t.kind === 'grammar') S.grev++; if (S.lastGrammar === id) S.lastGrammar = null; if (S.lastInput === id) S.lastInput = null; ensureLast(); if (S.active === id) { var next = S.tabs[Math.min(idx, S.tabs.length - 1)]; S.active = null; if (next) activate(next.id); else showEmpty(); } else { renderTabs(); refreshPanels(); } rememberSession(); scheduleRun(300); } if (!t.dirty || force) { proceed(); return Promise.resolve(true); } return ask('Unsaved changes', '"' + tabTitle(t) + '" has unsaved changes.', [{ label: 'Save', value: 'save', primary: true }, { label: "Don't save", value: 'discard' }, { label: 'Cancel', value: null }]) .then(function (v) { if (v === 'discard') { proceed(); return true; } if (v === 'save') return saveTab(t).then(function (ok) { if (ok) proceed(); return ok; }); return false; }); } function updatePos() { var t = getTab(S.active); var p = $('pos'); if (!t) { p.textContent = ''; return; } var c = caretInfo(t.ed); p.textContent = 'Ln ' + c.line + ', Col ' + c.col; } function rememberSession() { if (!S.info) return; var active = getTab(S.active); store.set('session:' + S.info.root, { open: S.tabs.filter(function (t) { return t.path; }).map(function (t) { return { path: t.path, kind: t.kind }; }), active: active ? active.path : '' }); } /* ========================================================== files on disk */ function openFile(path, o) { var ex = null; S.tabs.forEach(function (t) { if (t.path === path) ex = t; }); if (ex) { activate(ex.id); return Promise.resolve(ex); } return api('GET', '/api/file?path=' + encodeURIComponent(path)).then(function (f) { var t = addTab({ kind: (o && o.kind) || (isGrammarFile(f.path) ? 'grammar' : 'input'), path: f.path, text: f.text, etag: f.etag, eol: f.eol, bom: f.bom }); S.lastDir = dirname(f.path); if (f.lossy) toast(basename(f.path) + ' contains bytes that are not valid UTF-8; they were replaced.', 'error'); activate(t.id, !(o && o.noFocus)); return t; }).catch(function (e) { toast('Cannot open ' + path + ': ' + e.message, 'error'); return null; }); } function saveError(e) { toast('Save failed: ' + e.message, 'error'); return false; } function doSave(t, path, extra) { var text = t.ed.ta.value; var body = { path: path, text: text, eol: t.eol, bom: t.bom }; Object.keys(extra || {}).forEach(function (k) { body[k] = extra[k]; }); return api('POST', '/api/save', body).then(function (r) { t.path = r.path; t.saved = text; t.etag = r.etag; t.dirty = t.ed.ta.value !== text; t.banner = ''; S.lastDir = dirname(r.path); renderTabs(); updateBanner(t); rememberSession(); toast('Saved ' + basename(r.path)); var gn = t.kind === 'grammar' ? grammarName(text) : ''; if (gn && gn !== stem(basename(r.path))) { toast('ANTLR expects the file to be called ' + gn + '.g4 (the grammar is named ' + gn + ').', 'error'); } return true; }); } function saveTab(t) { if (!t.path) return saveAs(t); return doSave(t, t.path, { baseEtag: t.etag }).catch(function (e) { if (e.status === 409 && e.data && e.data.changed) { return ask('File changed on disk', basename(t.path) + ' was modified outside this editor after you opened it. Saving now overwrites those changes.', [{ label: 'Overwrite', value: 'o', primary: true }, { label: 'Cancel', value: null }]).then(function (v) { if (v !== 'o') return false; return doSave(t, t.path, { force: true }).catch(saveError); }); } return saveError(e); }); } function saveAs(t) { var suggest = t.path ? basename(t.path) : (t.kind === 'grammar' ? tabTitle(t) : 'input' + t.num + '.txt'); return fileDialog({ title: 'Save as', mode: 'save', dir: t.path ? dirname(t.path) : S.lastDir, name: suggest, okLabel: 'Save' }).then(function (path) { if (!path) return false; return doSave(t, path, { create: true }).catch(function (e) { if (e.status === 409 && e.data && e.data.exists) { return ask('Replace file?', path + ' already exists. Replace it?', [{ label: 'Replace', value: 'r', primary: true }, { label: 'Cancel', value: null }]) .then(function (v) { return v === 'r' ? doSave(t, path, {}).catch(saveError) : false; }); } return saveError(e); }); }); } function saveAll() { var chain = Promise.resolve(true); S.tabs.filter(function (t) { return t.dirty; }).forEach(function (t) { chain = chain.then(function () { return saveTab(t); }); }); return chain; } function reloadTab(t, quiet) { if (!t.path) return Promise.resolve(false); return api('GET', '/api/file?path=' + encodeURIComponent(t.path)).then(function (f) { var caret = Math.min(t.ed.ta.selectionStart, f.text.length); setEditorText(t.ed, f.text); t.ed.ta.setSelectionRange(caret, caret); t.saved = f.text; t.etag = f.etag; t.eol = f.eol; t.bom = f.bom; t.dirty = false; t.banner = ''; t.rev++; if (t.kind === 'grammar') S.grev++; renderTabs(); updateBanner(t); refreshPanels(); toast((quiet ? 'Reloaded ' + basename(t.path) + ' (changed on disk)' : 'Reloaded ' + basename(t.path))); if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(300); return true; }).catch(function (e) { toast('Cannot reload ' + t.path + ': ' + e.message, 'error'); return false; }); } function reloadActive() { var t = getTab(S.active); if (!t || !t.path) return; if (!t.dirty) { reloadTab(t, false); return; } ask('Reload from disk', 'Discard your unsaved changes to ' + tabTitle(t) + ' and reload it?', [{ label: 'Reload', value: 'r', primary: true }, { label: 'Cancel', value: null }]) .then(function (v) { if (v === 'r') reloadTab(t, false); }); } function newGrammar() { var t = addTab({ kind: 'grammar', text: NEW_GRAMMAR }); activate(t.id); scheduleRun(300); } function newInput() { var t = addTab({ kind: 'input', text: '' }); activate(t.id); } function updateBanner(t) { var b = t.ed.banner; clear(b); if (!t.banner) { b.hidden = true; return; } b.hidden = false; function btn(label, fn) { var x = el('button', 'btn sm', label); x.type = 'button'; x.onclick = fn; return x; } if (t.banner === 'deleted') { b.appendChild(el('span', '', 'This file was deleted on disk.')); b.appendChild(btn('Keep editing', function () { t.banner = ''; t.etag = ''; updateBanner(t); })); } else { b.appendChild(el('span', '', 'This file changed on disk.')); b.appendChild(btn('Reload', function () { reloadTab(t, false); })); b.appendChild(btn('Keep my version', function () { t.etag = t.diskEtag; t.banner = ''; updateBanner(t); })); } } /* ------------------------------------------------ external change polling */ var polling = false; function pollDisk() { if (polling || document.hidden) return; var paths = S.tabs.filter(function (t) { return t.path; }).map(function (t) { return t.path; }); if (!paths.length) return; polling = true; api('POST', '/api/stat', { paths: paths }).then(function (r) { S.tabs.forEach(function (t) { if (!t.path || !(t.path in r.etags)) return; var tag = r.etags[t.path]; if (tag === t.etag) return; if (tag === null) { if (t.banner !== 'deleted' && t.etag) { t.banner = 'deleted'; updateBanner(t); } return; } if (!t.dirty) { reloadTab(t, true); return; } if (t.diskEtag !== tag || t.banner !== 'changed') { t.diskEtag = tag; t.banner = 'changed'; updateBanner(t); } }); }).catch(function () { /* server gone or busy: try again later */ }).then(function () { polling = false; }); } setInterval(pollDisk, 2000); window.addEventListener('focus', pollDisk); window.addEventListener('beforeunload', function (ev) { if (S.tabs.some(function (t) { return t.dirty; })) { ev.preventDefault(); ev.returnValue = ''; } }); /* ======================================================== open / save dialog */ function fileDialog(o) { var cur = o.dir !== undefined && o.dir !== null ? o.dir : (S.lastDir || ''); var chosen = ''; var entries = []; var crumbs = el('div', 'crumbs'); var list = el('ul', 'flist'); list.setAttribute('role', 'listbox'); var filter = null, nameIn = null, ctxRef = null; function load(dir) { api('GET', '/api/dir?path=' + encodeURIComponent(dir)).then(function (d) { cur = d.path; entries = d.entries; chosen = ''; drawCrumbs(); drawList(); }).catch(function (e) { toast(e.message, 'error'); if (dir) load(''); }); } function crumb(label, path) { var b = el('button', '', label); b.type = 'button'; b.onclick = function () { load(path); }; return b; } function drawCrumbs() { clear(crumbs); crumbs.appendChild(crumb(S.info.rootName || 'workspace', '')); var acc = ''; (cur ? cur.split('/') : []).forEach(function (p) { acc = joinPath(acc, p); crumbs.appendChild(el('span', 'muted', '/')); crumbs.appendChild(crumb(p, acc)); }); } function drawList() { clear(list); var q = filter ? filter.value.toLowerCase() : ''; var shown = entries.filter(function (e) { return !q || e.name.toLowerCase().indexOf(q) >= 0; }); if (!shown.length) list.appendChild(el('li', 'muted', entries.length ? 'No matches' : 'Empty folder')); shown.forEach(function (e) { var li = el('li'); li.setAttribute('role', 'option'); li.tabIndex = 0; li.appendChild(el('span', '', (e.dir ? '\u25b8 ' : '') + e.name)); if (!e.dir) li.appendChild(el('span', 'sz', human(e.size))); var path = joinPath(cur, e.name); function select() { Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-selected'); }); li.setAttribute('aria-selected', 'true'); chosen = path; if (nameIn) nameIn.value = e.name; } li.onclick = function () { if (e.dir) load(path); else select(); }; li.ondblclick = function () { if (!e.dir) { select(); ctxRef.close('ok'); } }; li.onkeydown = function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); ev.stopPropagation(); if (e.dir) load(path); else { select(); ctxRef.close('ok'); } } else if (ev.key === ' ') { ev.preventDefault(); if (e.dir) load(path); else select(); } else if (ev.key === 'ArrowDown' && li.nextSibling) { ev.preventDefault(); li.nextSibling.focus(); } else if (ev.key === 'ArrowUp' && li.previousSibling) { ev.preventDefault(); li.previousSibling.focus(); } }; list.appendChild(li); }); } return modal({ title: o.title, build: function (ctx) { ctxRef = ctx; var wrap = el('div'); wrap.appendChild(crumbs); if (o.mode === 'open') { filter = el('input'); filter.type = 'text'; filter.placeholder = 'Filter'; filter.setAttribute('aria-label', 'Filter files'); filter.setAttribute('data-focus', '1'); filter.oninput = drawList; filter.onkeydown = function (ev) { if (ev.key === 'ArrowDown' && list.firstChild) { ev.preventDefault(); list.firstChild.focus(); } }; wrap.appendChild(filter); } wrap.appendChild(list); if (o.mode === 'save') { var lab = el('p', 'muted', 'File name'); lab.style.margin = '10px 0 4px'; wrap.appendChild(lab); nameIn = el('input'); nameIn.type = 'text'; nameIn.value = o.name || ''; nameIn.setAttribute('aria-label', 'File name'); nameIn.setAttribute('data-focus', '1'); wrap.appendChild(nameIn); setTimeout(function () { nameIn.focus(); nameIn.select(); }, 0); } load(cur); return wrap; }, buttons: [ { label: 'Cancel', value: null }, { label: o.okLabel || 'Open', value: 'ok', primary: true, check: function () { if (o.mode === 'open' && !chosen) { toast('Select a file first'); return false; } if (o.mode === 'save') { var n = nameIn.value.trim(); if (!n || /[\/\\]/.test(n)) { toast('Enter a file name without slashes'); return false; } } return true; } } ] }).then(function (v) { if (v !== 'ok') return null; S.lastDir = cur; return o.mode === 'open' ? chosen : joinPath(cur, nameIn.value.trim()); }); } function openDialog() { var t = getTab(S.active); fileDialog({ title: 'Open file', mode: 'open', dir: t && t.path ? dirname(t.path) : S.lastDir, okLabel: 'Open' }).then(function (p) { if (p) openFile(p); }); }