tricu

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

module-inspector.ts (5081B)


      1 import type { SourceEditor } from './source-editor.js';
      2 import { importSites } from './import-folds.js';
      3 import { loadLibraries, renderLibraryExports } from './library-browser.js';
      4 
      5 export function moduleInspector(input: SourceEditor, local: () => { label: string; manifest: string; names: string[] }[]) {
      6   const card = document.createElement('section'); card.className = 'definition-card module-card'; card.hidden = true;
      7   card.setAttribute('aria-label', 'Imported module');
      8   const heading = document.createElement('div'); heading.className = 'definition-card-controls';
      9   const title = document.createElement('strong'), close = document.createElement('button');
     10   close.type = 'button'; close.textContent = '×'; close.className = 'definition-card-close'; close.setAttribute('aria-label', 'Close module inspection');
     11   const search = document.createElement('input'); search.type = 'search'; search.placeholder = 'Filter definitions…'; search.setAttribute('aria-label', 'Search module definitions');
     12   const list = document.createElement('div'); list.className = 'module-exports';
     13   const status = document.createElement('p'); status.className = 'hint'; status.setAttribute('role', 'status');
     14   const identity = document.createElement('div'); identity.className = 'inspection-title';
     15   const kind = document.createElement('span'); kind.className = 'inspection-kind'; kind.textContent = 'Imported module';
     16   const description = document.createElement('p'); description.className = 'module-description'; description.hidden = true;
     17   identity.append(kind, title); heading.append(identity, close); card.append(heading, description, search, list, status); input.dom.append(card);
     18   let selected: {hash: string; from: number} | undefined, names: string[] = [], revision = 0;
     19   function hide(): void { revision++; selected = undefined; card.hidden = true; }
     20   function position(): void {
     21     if (!selected) return;
     22     const site = importSites(input.view.state).find(site => site.from === selected!.from && site.hash === selected!.hash);
     23     if (!site || input.view.state.doc.lineAt(input.view.state.selection.main.head).number !== site.line) { hide(); return; }
     24     const rect = input.view.coordsAtPos(site.to), height = input.dom.clientHeight;
     25     card.style.maxHeight = `${Math.max(80, Math.min(height - 16, innerHeight * .75, 400))}px`;
     26     const top = rect ? rect.bottom - input.dom.getBoundingClientRect().top + 4 : 8;
     27     card.style.top = `${Math.max(8, Math.min(top, height - card.offsetHeight - 8))}px`;
     28   }
     29   function render(): void {
     30     renderLibraryExports(list, names, search.value, name => {
     31       void navigator.clipboard.writeText(name).then(() => { status.textContent = 'Definition name copied.'; }, () => { status.textContent = name; });
     32     });
     33     position();
     34   }
     35   async function show(target: HTMLElement): Promise<void> {
     36     const hash = target.dataset.importHash;
     37     const site = importSites(input.view.state).find(site => site.hash === hash && site.line === input.view.state.doc.lineAt(input.view.state.selection.main.head).number);
     38     if (!site) return;
     39     const current = ++revision;
     40     selected = { hash: site.hash, from: site.from }; names = []; search.value = ''; list.replaceChildren(); description.hidden = true; description.textContent = '';
     41     title.textContent = 'Imported module'; status.textContent = 'Loading module definitions…'; card.hidden = false; position(); search.focus({preventScroll:true});
     42     try {
     43       const nearby = local().find(row => row.manifest === hash);
     44       const published = nearby ? undefined : (await loadLibraries()).find(row => row.manifest === hash);
     45       if (current !== revision) return;
     46       title.textContent = nearby?.label ?? published?.title ?? 'Imported module';
     47       description.textContent = published?.description ?? (nearby ? 'Local module' : ''); description.hidden = !description.textContent;
     48       names = nearby?.names ?? published?.names ?? [];
     49       status.textContent = nearby || published ? `${names.length} definitions · Click a name to copy it.` : 'This module is not in the library index or local modules.';
     50       render();
     51     } catch (error) { if (current === revision) status.textContent = `Could not load module definitions: ${String(error)}`; }
     52   }
     53   input.dom.addEventListener('click', event => {
     54     const target = (event.target as Element).closest<HTMLElement>('.import-hash-full');
     55     if (target && event.detail <= 1) void show(target);
     56   });
     57   input.dom.addEventListener('keydown', event => {
     58     const target = (event.target as Element).closest<HTMLElement>('.import-hash-full');
     59     if (target && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); event.stopPropagation(); void show(target); }
     60   }, true);
     61   function dismiss(): void { hide(); input.focus(); }
     62   close.onclick = dismiss;
     63   card.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); dismiss(); } });
     64   search.oninput = render; input.onLayout(position); input.onChange(hide);
     65   new ResizeObserver(position).observe(card);
     66   return { hide };
     67 }