tricu

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

definition-inspector.ts (15776B)


      1 import type { SourceEditor } from './source-editor.js';
      2 export { definitionSites } from './syntax.js';
      3 import { outputType, valueText, type OutputType, type ValueView } from './codecs.js';
      4 import { interpretationLabels, type InspectionFormat } from './auto-interpretation.js';
      5 import type { WorkerMessage, InspectionReply } from './protocol.js';
      6 import type { CompilerMessage } from './compiler-protocol.js';
      7 interface IOControls {
      8   run(name: string, output: OutputType, step: boolean): void;
      9   stop(): void;
     10   resume(): void;
     11 }
     12 
     13 export { valueText as inspectionText } from './codecs.js';
     14 export function definitionInspector(input: SourceEditor, request: (id: number, name: string, output: InspectionFormat) => void, io: IOControls) {
     15   const wrapper = input.dom;
     16   const card = document.createElement('section'); card.className = 'definition-card'; card.hidden = true;
     17   card.id = 'inline-definition'; card.setAttribute('aria-label', 'Compiled definition');
     18   const title = document.createElement('strong'), format = document.createElement('select');
     19   format.setAttribute('aria-label', 'Definition interpretation');
     20   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']]) format.add(new Option(label, value));
     21   const close = document.createElement('button'); close.className = 'definition-card-close'; close.textContent = '×'; close.type = 'button';
     22   close.setAttribute('aria-label', 'Close definition inspection'); close.title = 'Close (Escape)';
     23   const heading = document.createElement('div'); heading.className = 'definition-card-controls'; const identity = document.createElement('div'); identity.className = 'inspection-title';
     24   const kind = document.createElement('span'); kind.className = 'inspection-kind'; kind.textContent = 'Definition';
     25   identity.append(kind, title); heading.append(identity, format, close);
     26   const status = document.createElement('p'); status.className = 'hint'; status.setAttribute('role', 'status');
     27   const result = document.createElement('pre'); result.tabIndex = 0; result.setAttribute('aria-label', 'Compiled definition result');
     28   card.append(heading, result, status); wrapper.append(card);
     29   const actions = document.createElement('div'); actions.className = 'definition-actions';
     30   const copy = document.createElement('button'), download = document.createElement('button');
     31   copy.textContent = 'Copy'; download.textContent = 'Download'; const edit = document.createElement('button'); edit.type = 'button'; edit.textContent = 'Edit name';
     32   actions.append(copy, download); card.append(actions, edit); edit.className = 'edit-definition-name'; edit.hidden = true;
     33   const ioPanel = document.createElement('section'); ioPanel.className = 'inline-io'; ioPanel.hidden = true;
     34   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>
     35     <p class="hint">Recognized tricuIO envelope. Only Run IO executes actions.</p>
     36     <div id="terminal-io"></div>
     37     <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>
     38     <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>`;
     39   card.append(ioPanel);
     40   const get = <T extends HTMLElement>(id: string) => ioPanel.querySelector<T>('#' + id)!;
     41   const run = get<HTMLButtonElement>('run-io'), stop = get<HTMLButtonElement>('stop-io');
     42   const next = get<HTMLButtonElement>('next'), trace = get('trace');
     43   const returnFormat = get<HTMLSelectElement>('io-return-format'), step = get<HTMLInputElement>('step-mode');
     44   let saved: { text: string; filename: string } | undefined, ioDetected = false, ioRunning = false, suspended = false;
     45   function save(value: ValueView): void {
     46     const text = valueText(value);
     47     const name = (selected ?? 'definition').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
     48     saved = { text, filename: 'file' in value ? `${name}.${value.file.extension}` : `${name}.${value.type === 'bytes' ? 'json' : 'txt'}` };
     49     result.textContent = text.slice(0, 16_384);
     50     if (text.length > 16_384) status.textContent += ' · Preview: first 16,384 characters. Copy/Download include the complete output.';
     51     actions.hidden = false; copy.disabled = download.disabled = false;
     52   }
     53   function clear(): void { actions.hidden = true; saved = undefined; copy.disabled = download.disabled = true; result.textContent = ''; }
     54   copy.onclick = async () => {
     55     const value = saved; if (!value) return;
     56     try { await navigator.clipboard.writeText(value.text); }
     57     catch { if (saved === value) status.textContent += ' · Clipboard unavailable; use Download.'; }
     58   };
     59   download.onclick = () => {
     60     if (!saved) return;
     61     const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
     62     const anchor = document.createElement('a'); anchor.href = url; anchor.download = saved.filename;
     63     document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);
     64   };
     65   run.onclick = () => {
     66     if (!selected || !ready || busy || !ioDetected) return;
     67     ioRunning = true; clear(); get('terminal-io').replaceChildren(); trace.replaceChildren();
     68     status.textContent = 'Running IO…'; io.run(selected, outputType(returnFormat.value), step.checked);
     69   };
     70   stop.onclick = () => io.stop(); next.onclick = () => io.resume();
     71   clear();
     72   let selected: string | undefined, source = '', ready = false, busy = false, serial = 0;
     73   let pending: number | undefined, last = '', generation = 0, wasReady = false, scheduled = false;
     74   let published:string[]|undefined;
     75   const wired = new WeakSet<HTMLElement>();
     76   function selectName(name: string): void {
     77     if (busy || input.isDisabled || input.isReadOnly || wrapper.classList.contains('composing')) return;
     78     const site = input.declarations().find(site => site.name === name);
     79     if (site) { hide(); input.select(site.start, site.start + site.name.length); }
     80   }
     81   function wireTarget(target: HTMLElement): void {
     82     if (wired.has(target)) return; wired.add(target);
     83     const name = () => target.dataset.definition ?? '';
     84     const enabled = () => ready && !busy && target.matches('.definition-name[aria-disabled="false"]') && !wrapper.classList.contains('composing');
     85     let gesture: {x:number;y:number;time:number;cancelled:boolean;moved:boolean;touch:boolean}|undefined;
     86     let hold:ReturnType<typeof setTimeout>|undefined;
     87     const cancel = () => {clearTimeout(hold);hold=undefined;};
     88     target.onpointerdown = event => {
     89       cancel(); const current={x:event.clientX,y:event.clientY,time:performance.now(),cancelled:false,moved:false,touch:event.pointerType==='touch'||event.pointerType==='pen'}; gesture=current;
     90       // Short taps inspect; holds select the identifier for editing. Pointer
     91       // cancellation preserves native scrolling.
     92       if(current.touch) {event.preventDefault();hold=setTimeout(()=>{if(gesture===current&&!current.cancelled&&enabled()){current.cancelled=true;selectName(name());}},500);}
     93 
     94     };
     95     target.onpointermove = event => {if(gesture&&Math.hypot(event.clientX-gesture.x,event.clientY-gesture.y)>8){gesture.cancelled=gesture.moved=true;cancel();}};
     96     target.onpointerup = () => {cancel();if(gesture?.touch&&!gesture.moved&&performance.now()-gesture.time>=500&&enabled()){gesture.cancelled=true;selectName(name());}};
     97     target.onpointercancel = () => {if(gesture)gesture.cancelled=true;cancel();};
     98     target.oncontextmenu = () => {cancel();if(gesture){if(gesture.touch&&!gesture.moved&&enabled())selectName(name());gesture.cancelled=true;}};
     99     target.onclick = event => {
    100       cancel(); if(event.detail&&gesture&&(gesture.cancelled||performance.now()-gesture.time>500))return;
    101       if(!enabled())return;
    102       const selection=document.getSelection();
    103       if(event.detail>1)return;
    104       // Touch gesture state distinguishes taps, holds and pans even when the
    105       // browser retains a previous DOM selection.
    106       if(event.detail&&!gesture?.touch&&selection&&!selection.isCollapsed&&(target.contains(selection.anchorNode)||target.contains(selection.focusNode)))return;
    107       choose(name());if(!card.hidden)result.focus({preventScroll:true});
    108     };
    109     target.addEventListener('keydown',event=>{
    110       if(['Enter',' '].includes(event.key)){event.preventDefault();event.stopPropagation();if(enabled()){choose(name());if(!card.hidden)result.focus({preventScroll:true});}}
    111       if(event.key==='Escape'){event.preventDefault();event.stopPropagation();hide();}
    112     },true);
    113   }
    114   function position(): void {
    115     input.setDefinitions({ready,busy,...(selected&&!card.hidden?{selected}:{}),...(published?{published}:{})});
    116     for(const target of wrapper.querySelectorAll<HTMLElement>('.definition-name'))wireTarget(target);
    117     const available=selected&&input.declarations().some(site=>site.name===selected);
    118     edit.hidden = input.dom.hidden || !available; edit.disabled = busy || input.isDisabled || input.isReadOnly;
    119     edit.title = input.isReadOnly ? 'This source is read-only.' : 'Select the declaration name in the editor.';
    120     const height=input.dom.clientHeight;
    121     card.style.maxHeight = `${input.dom.hidden ? Math.min(540,innerHeight*.75) : Math.min(height-16,innerHeight*.75,ioDetected?540:400)}px`;
    122     if(selected&&!card.hidden&&!input.dom.hidden){
    123       const site=input.declarations().find(site=>site.name===selected);
    124       const rect=site?input.view.coordsAtPos(site.start+site.name.length):null;
    125       const top=rect?rect.bottom-wrapper.getBoundingClientRect().top+4:8;
    126       card.style.top=`${Math.max(8,Math.min(top,height-card.offsetHeight-8))}px`;
    127     }
    128   }
    129   function inspect(): void {
    130     if (!selected || !ready || busy || suspended || pending !== undefined) return;
    131     const key = `${generation}:${selected}:${format.value}`; if (last === key) return;
    132     last = key; pending = ++serial; status.textContent = 'Reading compiled tree… Stop is available in the toolbar.'; result.textContent = '';
    133     request(pending, selected, format.value === 'auto' ? 'auto' : outputType(format.value));
    134   }
    135   function schedule(): void {
    136     if (scheduled) return; scheduled = true;
    137     queueMicrotask(() => { scheduled = false; inspect(); });
    138   }
    139   function choose(name: string): void {
    140     if (selected === name && !card.hidden && !suspended) { hide(); return; }
    141     selected = name; suspended = false; ioDetected = false; next.disabled = true; ioPanel.hidden = true; get('terminal-io').replaceChildren(); trace.replaceChildren(); clear(); format.value = 'auto'; title.textContent = name; card.hidden = false; last = ''; result.textContent = '';
    142     position(); schedule();
    143   }
    144   function hide(): void {
    145     if (ioRunning) io.stop();
    146     const name = selected; selected = undefined; card.hidden = true;
    147     position();
    148     const target=[...wrapper.querySelectorAll<HTMLElement>('.definition-name')].find(target=>target.dataset.definition===name&&target.getAttribute('aria-disabled')==='false');
    149     if(target)target.focus({preventScroll:true});else if(busy)document.getElementById('stop')?.focus({preventScroll:true});
    150 
    151   }
    152   close.onclick = hide;
    153   card.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); hide(); } });
    154   edit.onclick = () => { if (selected && !input.isReadOnly) selectName(selected); };
    155   format.onchange = () => { suspended = false; last = ''; schedule(); };
    156   function update(current: boolean, working: boolean, publishedNames?: string[]): void {
    157     ready = current; busy = working; published=publishedNames;
    158     if (!input.dom.hidden && card.parentElement !== wrapper) { wrapper.append(card); card.classList.remove('external-definition'); }
    159     if (ready && !wasReady) { generation++; last = ''; }
    160     wasReady = ready;
    161     if (source !== input.getSource()) {
    162       ioDetected = false; ioPanel.hidden = true; clear();
    163       source = input.getSource();
    164       if(selected&&!input.declarations().some(site=>site.name===selected)){selected=undefined;card.hidden=true;result.textContent='';}
    165     }
    166 
    167     if (selected && publishedNames && !publishedNames.includes(selected)) {
    168       selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = '';
    169     }
    170     format.disabled = !ready || busy;
    171     run.disabled = !ready || busy || !ioDetected; stop.disabled = !ioRunning; returnFormat.disabled = step.disabled = busy;
    172     copy.disabled = download.disabled = busy || !saved;
    173     if (!ready) {
    174       pending = undefined; last = ''; ioDetected = false; ioPanel.hidden = true; clear();
    175       if (selected) { status.textContent = suspended ? 'Runtime stopped. Recompile/reload, then click the definition name to inspect again.' : 'Source changed or runtime unavailable. Waiting for successful compilation.'; result.textContent = ''; }
    176     }
    177     position(); schedule();
    178   }
    179   input.onLayout(position);
    180   new ResizeObserver(position).observe(card);
    181   return {
    182     open(name: string): void { card.classList.toggle('external-definition', input.dom.hidden); if (input.dom.hidden) { card.style.top = ''; document.getElementById('package-definition-view')!.append(card); } else wrapper.append(card); choose(name); },
    183     event(message: WorkerMessage | CompilerMessage): void {
    184       if (!ioRunning) return;
    185       if (message.type === 'trace') { const row = document.createElement('li'); row.textContent = `${message.event.step} ${message.event.name}: ${message.event.detail}`; trace.append(row); }
    186       if (message.type === 'state') { next.disabled = message.state !== 'step'; status.textContent = message.state === 'step' ? 'Paused before action' : 'Running IO…'; }
    187       if (message.type === 'done') { ioRunning = false; next.disabled = true; status.textContent = 'IO finished · Returned value'; save(message.result); }
    188       position();
    189     },
    190     // Stop suspends observation until the user reopens the definition.
    191     stopped(message: string, suspend = false): void { suspended = suspend; ioRunning = false; next.disabled = true; stop.disabled = true; status.textContent = message; },
    192     reset(): void { suspended = false; ioRunning = false; ioDetected = false; ioPanel.hidden = true; clear(); selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; position(); },
    193     update,
    194     reply(message: InspectionReply): boolean {
    195       if (message.id !== pending) return false;
    196       pending = undefined;
    197       if (!selected || !ready) return true;
    198       if (message.type === 'inspection-error') { status.textContent = `Cannot interpret this tree: ${message.message}`; clear(); }
    199       else if ('io' in message && message.io) { ioDetected = true; ioPanel.hidden = false; status.textContent = 'IO program · authenticated tricuIO envelope'; clear(); }
    200       else if ('result' in message) { status.textContent = format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0]!.textContent; save(message.result); }
    201       position(); return true;
    202     },
    203   };
    204 }