tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

definition-inspector.js (17696B)


      1 export { definitionSites } from './syntax.js';
      2 import { outputType, valueText } from './codecs.js';
      3 import { interpretationLabels } from './auto-interpretation.js';
      4 export { valueText as inspectionText } from './codecs.js';
      5 export function definitionInspector(input, request, io) {
      6     const wrapper = input.dom;
      7     const card = document.createElement('section');
      8     card.className = 'definition-card';
      9     card.hidden = true;
     10     card.id = 'inline-definition';
     11     card.setAttribute('aria-label', 'Compiled definition');
     12     const title = document.createElement('strong'), format = document.createElement('select');
     13     format.setAttribute('aria-label', 'Definition interpretation');
     14     for (const [value, label] of [['auto', 'Auto'], ['tricu', 'tricu · t source'], ['triangle', '△ source'], ['dag', 'DAG · shared △'], ['ternary', 'Ternary'], ['string', 'Text'], ['number', 'Natural number'], ['bool', 'Boolean'], ['bytes', 'Bytes'], ['values', 'List · text / numbers']])
     15         format.add(new Option(label, value));
     16     const close = document.createElement('button');
     17     close.className = 'definition-card-close';
     18     close.textContent = '×';
     19     close.type = 'button';
     20     close.setAttribute('aria-label', 'Close definition inspection');
     21     close.title = 'Close (Escape)';
     22     const heading = document.createElement('div');
     23     heading.className = 'definition-card-controls';
     24     const identity = document.createElement('div');
     25     identity.className = 'inspection-title';
     26     const kind = document.createElement('span');
     27     kind.className = 'inspection-kind';
     28     kind.textContent = 'Definition';
     29     identity.append(kind, title);
     30     heading.append(identity, format, close);
     31     const status = document.createElement('p');
     32     status.className = 'hint';
     33     status.setAttribute('role', 'status');
     34     const result = document.createElement('pre');
     35     result.tabIndex = 0;
     36     result.setAttribute('aria-label', 'Compiled definition result');
     37     card.append(heading, result, status);
     38     wrapper.append(card);
     39     const actions = document.createElement('div');
     40     actions.className = 'definition-actions';
     41     const copy = document.createElement('button'), download = document.createElement('button');
     42     copy.textContent = 'Copy';
     43     download.textContent = 'Download';
     44     const edit = document.createElement('button');
     45     edit.type = 'button';
     46     edit.textContent = 'Edit name';
     47     actions.append(copy, download);
     48     card.append(actions, edit);
     49     edit.className = 'edit-definition-name';
     50     edit.hidden = true;
     51     const ioPanel = document.createElement('section');
     52     ioPanel.className = 'inline-io';
     53     ioPanel.hidden = true;
     54     ioPanel.innerHTML = `<div class="inline-io-actions"><button type="button" id="run-io">Run IO</button><button type="button" id="stop-io" disabled>Stop</button></div>
     55     <p class="hint">Recognized tricuIO envelope. Only Run IO executes actions.</p>
     56     <div id="terminal-io"></div>
     57     <details><summary>IO details</summary><label><input id="step-mode" type="checkbox"> Pause before each scheduler turn</label><button id="next" type="button" disabled>Next scheduler turn</button>
     58     <label for="io-return-format">Returned value (next run)</label><select id="io-return-format"><option value="tricu">tricu source</option><option value="string">Text</option><option value="number">Natural number</option><option value="values">List · text / numbers</option><option value="bool">Boolean</option><option value="bytes">Bytes</option><option value="triangle">△ source</option><option value="ternary">Ternary</option><option value="dag">DAG</option></select><ol id="trace"></ol></details>`;
     59     card.append(ioPanel);
     60     const get = (id) => ioPanel.querySelector('#' + id);
     61     const run = get('run-io'), stop = get('stop-io');
     62     const next = get('next'), trace = get('trace');
     63     const returnFormat = get('io-return-format'), step = get('step-mode');
     64     let saved, ioDetected = false, ioRunning = false, suspended = false;
     65     function save(value) {
     66         const text = valueText(value);
     67         const name = (selected ?? 'definition').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
     68         saved = { text, filename: 'file' in value ? `${name}.${value.file.extension}` : `${name}.${value.type === 'bytes' ? 'json' : 'txt'}` };
     69         result.textContent = text.slice(0, 16_384);
     70         if (text.length > 16_384)
     71             status.textContent += ' · Preview: first 16,384 characters. Copy/Download include the complete output.';
     72         actions.hidden = false;
     73         copy.disabled = download.disabled = false;
     74     }
     75     function clear() { actions.hidden = true; saved = undefined; copy.disabled = download.disabled = true; result.textContent = ''; }
     76     copy.onclick = async () => {
     77         const value = saved;
     78         if (!value)
     79             return;
     80         try {
     81             await navigator.clipboard.writeText(value.text);
     82         }
     83         catch {
     84             if (saved === value)
     85                 status.textContent += ' · Clipboard unavailable; use Download.';
     86         }
     87     };
     88     download.onclick = () => {
     89         if (!saved)
     90             return;
     91         const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
     92         const anchor = document.createElement('a');
     93         anchor.href = url;
     94         anchor.download = saved.filename;
     95         document.body.append(anchor);
     96         anchor.click();
     97         anchor.remove();
     98         setTimeout(() => URL.revokeObjectURL(url), 1000);
     99     };
    100     run.onclick = () => {
    101         if (!selected || !ready || busy || !ioDetected)
    102             return;
    103         ioRunning = true;
    104         clear();
    105         get('terminal-io').replaceChildren();
    106         trace.replaceChildren();
    107         status.textContent = 'Running IO…';
    108         io.run(selected, outputType(returnFormat.value), step.checked);
    109     };
    110     stop.onclick = () => io.stop();
    111     next.onclick = () => io.resume();
    112     clear();
    113     let selected, source = '', ready = false, busy = false, serial = 0;
    114     let pending, last = '', generation = 0, wasReady = false, scheduled = false;
    115     let published;
    116     const wired = new WeakSet();
    117     function selectName(name) {
    118         if (busy || input.isDisabled || input.isReadOnly || wrapper.classList.contains('composing'))
    119             return;
    120         const site = input.declarations().find(site => site.name === name);
    121         if (site) {
    122             hide();
    123             input.select(site.start, site.start + site.name.length);
    124         }
    125     }
    126     function wireTarget(target) {
    127         if (wired.has(target))
    128             return;
    129         wired.add(target);
    130         const name = () => target.dataset.definition ?? '';
    131         const enabled = () => ready && !busy && target.matches('.definition-name[aria-disabled="false"]') && !wrapper.classList.contains('composing');
    132         let gesture;
    133         let hold;
    134         const cancel = () => { clearTimeout(hold); hold = undefined; };
    135         target.onpointerdown = event => {
    136             cancel();
    137             const current = { x: event.clientX, y: event.clientY, time: performance.now(), cancelled: false, moved: false, touch: event.pointerType === 'touch' || event.pointerType === 'pen' };
    138             gesture = current;
    139             // Short taps inspect; holds select the identifier for editing. Pointer
    140             // cancellation preserves native scrolling.
    141             if (current.touch) {
    142                 event.preventDefault();
    143                 hold = setTimeout(() => { if (gesture === current && !current.cancelled && enabled()) {
    144                     current.cancelled = true;
    145                     selectName(name());
    146                 } }, 500);
    147             }
    148         };
    149         target.onpointermove = event => { if (gesture && Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) > 8) {
    150             gesture.cancelled = gesture.moved = true;
    151             cancel();
    152         } };
    153         target.onpointerup = () => { cancel(); if (gesture?.touch && !gesture.moved && performance.now() - gesture.time >= 500 && enabled()) {
    154             gesture.cancelled = true;
    155             selectName(name());
    156         } };
    157         target.onpointercancel = () => { if (gesture)
    158             gesture.cancelled = true; cancel(); };
    159         target.oncontextmenu = () => { cancel(); if (gesture) {
    160             if (gesture.touch && !gesture.moved && enabled())
    161                 selectName(name());
    162             gesture.cancelled = true;
    163         } };
    164         target.onclick = event => {
    165             cancel();
    166             if (event.detail && gesture && (gesture.cancelled || performance.now() - gesture.time > 500))
    167                 return;
    168             if (!enabled())
    169                 return;
    170             const selection = document.getSelection();
    171             if (event.detail > 1)
    172                 return;
    173             // Touch gesture state distinguishes taps, holds and pans even when the
    174             // browser retains a previous DOM selection.
    175             if (event.detail && !gesture?.touch && selection && !selection.isCollapsed && (target.contains(selection.anchorNode) || target.contains(selection.focusNode)))
    176                 return;
    177             choose(name());
    178             if (!card.hidden)
    179                 result.focus({ preventScroll: true });
    180         };
    181         target.addEventListener('keydown', event => {
    182             if (['Enter', ' '].includes(event.key)) {
    183                 event.preventDefault();
    184                 event.stopPropagation();
    185                 if (enabled()) {
    186                     choose(name());
    187                     if (!card.hidden)
    188                         result.focus({ preventScroll: true });
    189                 }
    190             }
    191             if (event.key === 'Escape') {
    192                 event.preventDefault();
    193                 event.stopPropagation();
    194                 hide();
    195             }
    196         }, true);
    197     }
    198     function position() {
    199         input.setDefinitions({ ready, busy, ...(selected && !card.hidden ? { selected } : {}), ...(published ? { published } : {}) });
    200         for (const target of wrapper.querySelectorAll('.definition-name'))
    201             wireTarget(target);
    202         const available = selected && input.declarations().some(site => site.name === selected);
    203         edit.hidden = input.dom.hidden || !available;
    204         edit.disabled = busy || input.isDisabled || input.isReadOnly;
    205         edit.title = input.isReadOnly ? 'This source is read-only.' : 'Select the declaration name in the editor.';
    206         const height = input.dom.clientHeight;
    207         card.style.maxHeight = `${input.dom.hidden ? Math.min(540, innerHeight * .75) : Math.min(height - 16, innerHeight * .75, ioDetected ? 540 : 400)}px`;
    208         if (selected && !card.hidden && !input.dom.hidden) {
    209             const site = input.declarations().find(site => site.name === selected);
    210             const rect = site ? input.view.coordsAtPos(site.start + site.name.length) : null;
    211             const top = rect ? rect.bottom - wrapper.getBoundingClientRect().top + 4 : 8;
    212             card.style.top = `${Math.max(8, Math.min(top, height - card.offsetHeight - 8))}px`;
    213         }
    214     }
    215     function inspect() {
    216         if (!selected || !ready || busy || suspended || pending !== undefined)
    217             return;
    218         const key = `${generation}:${selected}:${format.value}`;
    219         if (last === key)
    220             return;
    221         last = key;
    222         pending = ++serial;
    223         status.textContent = 'Reading compiled tree… Stop is available in the toolbar.';
    224         result.textContent = '';
    225         request(pending, selected, format.value === 'auto' ? 'auto' : outputType(format.value));
    226     }
    227     function schedule() {
    228         if (scheduled)
    229             return;
    230         scheduled = true;
    231         queueMicrotask(() => { scheduled = false; inspect(); });
    232     }
    233     function choose(name) {
    234         if (selected === name && !card.hidden && !suspended) {
    235             hide();
    236             return;
    237         }
    238         selected = name;
    239         suspended = false;
    240         ioDetected = false;
    241         next.disabled = true;
    242         ioPanel.hidden = true;
    243         get('terminal-io').replaceChildren();
    244         trace.replaceChildren();
    245         clear();
    246         format.value = 'auto';
    247         title.textContent = name;
    248         card.hidden = false;
    249         last = '';
    250         result.textContent = '';
    251         position();
    252         schedule();
    253     }
    254     function hide() {
    255         if (ioRunning)
    256             io.stop();
    257         const name = selected;
    258         selected = undefined;
    259         card.hidden = true;
    260         position();
    261         const target = [...wrapper.querySelectorAll('.definition-name')].find(target => target.dataset.definition === name && target.getAttribute('aria-disabled') === 'false');
    262         if (target)
    263             target.focus({ preventScroll: true });
    264         else if (busy)
    265             document.getElementById('stop')?.focus({ preventScroll: true });
    266     }
    267     close.onclick = hide;
    268     card.addEventListener('keydown', event => { if (event.key === 'Escape') {
    269         event.preventDefault();
    270         hide();
    271     } });
    272     edit.onclick = () => { if (selected && !input.isReadOnly)
    273         selectName(selected); };
    274     format.onchange = () => { suspended = false; last = ''; schedule(); };
    275     function update(current, working, publishedNames) {
    276         ready = current;
    277         busy = working;
    278         published = publishedNames;
    279         if (!input.dom.hidden && card.parentElement !== wrapper) {
    280             wrapper.append(card);
    281             card.classList.remove('external-definition');
    282         }
    283         if (ready && !wasReady) {
    284             generation++;
    285             last = '';
    286         }
    287         wasReady = ready;
    288         if (source !== input.getSource()) {
    289             ioDetected = false;
    290             ioPanel.hidden = true;
    291             clear();
    292             source = input.getSource();
    293             if (selected && !input.declarations().some(site => site.name === selected)) {
    294                 selected = undefined;
    295                 card.hidden = true;
    296                 result.textContent = '';
    297             }
    298         }
    299         if (selected && publishedNames && !publishedNames.includes(selected)) {
    300             selected = undefined;
    301             pending = undefined;
    302             last = '';
    303             card.hidden = true;
    304             result.textContent = '';
    305         }
    306         format.disabled = !ready || busy;
    307         run.disabled = !ready || busy || !ioDetected;
    308         stop.disabled = !ioRunning;
    309         returnFormat.disabled = step.disabled = busy;
    310         copy.disabled = download.disabled = busy || !saved;
    311         if (!ready) {
    312             pending = undefined;
    313             last = '';
    314             ioDetected = false;
    315             ioPanel.hidden = true;
    316             clear();
    317             if (selected) {
    318                 status.textContent = suspended ? 'Runtime stopped. Recompile/reload, then click the definition name to inspect again.' : 'Source changed or runtime unavailable. Waiting for successful compilation.';
    319                 result.textContent = '';
    320             }
    321         }
    322         position();
    323         schedule();
    324     }
    325     input.onLayout(position);
    326     new ResizeObserver(position).observe(card);
    327     return {
    328         open(name) { card.classList.toggle('external-definition', input.dom.hidden); if (input.dom.hidden) {
    329             card.style.top = '';
    330             document.getElementById('package-definition-view').append(card);
    331         }
    332         else
    333             wrapper.append(card); choose(name); },
    334         event(message) {
    335             if (!ioRunning)
    336                 return;
    337             if (message.type === 'trace') {
    338                 const row = document.createElement('li');
    339                 row.textContent = `${message.event.step} ${message.event.name}: ${message.event.detail}`;
    340                 trace.append(row);
    341             }
    342             if (message.type === 'state') {
    343                 next.disabled = message.state !== 'step';
    344                 status.textContent = message.state === 'step' ? 'Paused before action' : 'Running IO…';
    345             }
    346             if (message.type === 'done') {
    347                 ioRunning = false;
    348                 next.disabled = true;
    349                 status.textContent = 'IO finished · Returned value';
    350                 save(message.result);
    351             }
    352             position();
    353         },
    354         // Stop suspends observation until the user reopens the definition.
    355         stopped(message, suspend = false) { suspended = suspend; ioRunning = false; next.disabled = true; stop.disabled = true; status.textContent = message; },
    356         reset() { suspended = false; ioRunning = false; ioDetected = false; ioPanel.hidden = true; clear(); selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; position(); },
    357         update,
    358         reply(message) {
    359             if (message.id !== pending)
    360                 return false;
    361             pending = undefined;
    362             if (!selected || !ready)
    363                 return true;
    364             if (message.type === 'inspection-error') {
    365                 status.textContent = `Cannot interpret this tree: ${message.message}`;
    366                 clear();
    367             }
    368             else if ('io' in message && message.io) {
    369                 ioDetected = true;
    370                 ioPanel.hidden = false;
    371                 status.textContent = 'IO program · authenticated tricuIO envelope';
    372                 clear();
    373             }
    374             else if ('result' in message) {
    375                 status.textContent = format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0].textContent;
    376                 save(message.result);
    377             }
    378             position();
    379             return true;
    380         },
    381     };
    382 }