tricu

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

import-picker.js (12846B)


      1 import { loadLibraries, renderLibraryExports } from './library-browser.js';
      2 import { moduleInspector } from './module-inspector.js';
      3 import { latestLibraries, matchesLibrary } from './library-index.js';
      4 import { packageReleases } from './packages.js';
      5 export function importPicker(options) {
      6     const inspector = moduleInspector(options.source, options.local);
      7     const get = (id) => document.getElementById(id);
      8     const dialog = get('import-dialog'), open = get('add-import');
      9     const search = get('import-search'), results = get('import-results'), detail = get('import-detail');
     10     const alias = get('import-alias'), versions = get('import-version');
     11     const tryButton = get('try-library');
     12     const status = get('import-status'), preview = get('import-preview'), insert = get('insert-import');
     13     let libraries, selected, plan;
     14     let worker, timer, debounce;
     15     let revision = 0, snapshot = '', backend = '';
     16     const stop = () => { revision++; worker?.terminate(); worker = undefined; clearTimeout(timer); clearTimeout(debounce); };
     17     const finish = () => { stop(); dialog.close(); open.focus(); };
     18     const message = (text) => { status.textContent = text; };
     19     const statement = () => selected ? `!import ${selected.manifest} as ${alias.value}` : '';
     20     function check() {
     21         stop();
     22         plan = undefined;
     23         insert.disabled = true;
     24         tryButton.disabled = false;
     25         insert.textContent = 'Insert import';
     26         preview.textContent = statement();
     27         if (!selected)
     28             return;
     29         if (!options.editable()) {
     30             message('Browse references, copy an import, or open this library in the Playground.');
     31             return;
     32         }
     33         if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(alias.value)) {
     34             message('Choose a namespace using letters, digits and underscores, starting with a letter or underscore.');
     35             return;
     36         }
     37         message('Checking with the native compiler…');
     38         const current = revision;
     39         debounce = setTimeout(() => {
     40             if (!dialog.open || current !== revision)
     41                 return;
     42             const url = new URL('./wasm-worker.js', import.meta.url);
     43             url.searchParams.set('backend', backend);
     44             const next = new Worker(url, { type: 'module' });
     45             worker = next;
     46             const fail = (text) => { if (current !== revision)
     47                 return; stop(); message(`${text} You can copy the import and finish editing the source.`); };
     48             timer = setTimeout(() => fail('Import check timed out.'), 60_000);
     49             next.onerror = event => { event.preventDefault(); fail(event.message || 'Import check failed.'); };
     50             next.onmessage = ({ data }) => {
     51                 if (current !== revision || !dialog.open)
     52                     return;
     53                 if (data.type === 'error') {
     54                     fail(data.message ?? 'Import check failed.');
     55                     return;
     56                 }
     57                 if (data.type !== 'import-checked' || !data.plan)
     58                     return;
     59                 plan = data.plan;
     60                 stop();
     61                 insert.disabled = false;
     62                 tryButton.disabled = false;
     63                 insert.textContent = plan.existing ? 'Show existing import' : 'Insert import';
     64                 message(plan.existing ? `Already imported ${plan.existing.alias === '*' ? 'openly' : 'as ' + plan.existing.alias}.` : 'Ready to insert.');
     65             };
     66             next.postMessage({ type: 'check-import', source: snapshot, manifest: selected.manifest, alias: alias.value, localModules: options.inputs() });
     67         }, 250);
     68     }
     69     function show(choice, focus = true) {
     70         selected = choice;
     71         get('import-actions').hidden = false;
     72         results.hidden = true;
     73         get('import-browse').hidden = true;
     74         detail.hidden = false;
     75         get('import-title').textContent = choice.definition ? `${choice.definition} · ${choice.title}` : choice.title;
     76         get('import-description').textContent = choice.description;
     77         get('import-usage').replaceChildren(...(choice.library?.usage ?? '').split('`').map((text, i) => {
     78             if (i % 2 === 0)
     79                 return document.createTextNode(text);
     80             const code = document.createElement('code');
     81             code.textContent = text;
     82             return code;
     83         }));
     84         get('import-usage-details').hidden = !choice.library?.usage;
     85         get('import-example').textContent = choice.library?.example ?? '';
     86         get('import-example-details').hidden = !choice.library?.example;
     87         get('import-usage-details').open = false;
     88         get('import-example-details').open = false;
     89         get('import-definition').textContent = choice.definition ? `Definition: ${choice.definition} · from ${choice.title}. Adding an import makes the whole library available.` : 'Explore exports below. Imports make the whole library available.';
     90         tryButton.hidden = options.editable() || !choice.library;
     91         tryButton.textContent = 'Open library';
     92         get('copy-qualified').hidden = !choice.definition;
     93         alias.value = choice.alias;
     94         versions.replaceChildren();
     95         versions.hidden = !choice.library;
     96         get('import-version-label').hidden = !choice.library;
     97         if (choice.library)
     98             for (const release of packageReleases(libraries, choice.library.name)) {
     99                 const option = new Option(release.version, release.version);
    100                 option.selected = release.version === choice.library.version;
    101                 versions.add(option);
    102             }
    103         const reference = get('import-reference');
    104         reference.hidden = !choice.library;
    105         if (choice.library)
    106             reference.href = new URL('../reference/' + choice.library.reference, import.meta.url).href;
    107         get('import-export-search').value = '';
    108         renderExports();
    109         check();
    110         if (focus) {
    111             get('import-content').scrollTop = 0;
    112             get('import-title').focus({ preventScroll: true });
    113         }
    114     }
    115     function renderExports() {
    116         const query = get('import-export-search').value.toLowerCase();
    117         renderLibraryExports(get('import-exports'), selected?.names ?? [], query, name => {
    118             if (selected)
    119                 show({ ...selected, alias: alias.value, definition: name }, false);
    120         });
    121     }
    122     function browse() {
    123         stop();
    124         selected = undefined;
    125         plan = undefined;
    126         detail.hidden = true;
    127         get('import-actions').hidden = true;
    128         get('import-content').scrollTop = 0;
    129         get('import-browse').hidden = false;
    130         results.hidden = false;
    131         message('');
    132         results.replaceChildren();
    133         const choices = [
    134             ...options.local().map(row => ({ title: row.label, description: 'Local file', manifest: row.manifest, names: row.names, alias: 'Local' })),
    135             ...latestLibraries(libraries ?? []).map(row => ({ title: row.title, description: row.description, manifest: row.manifest, names: row.names, alias: row.alias, library: row })),
    136         ].filter(row => matchesLibrary(row, search.value));
    137         const heading = (text) => { const h = document.createElement('h3'); h.textContent = text; results.append(h); };
    138         const addChoice = (choice) => {
    139             const button = document.createElement('button');
    140             button.type = 'button';
    141             button.dataset['manifest'] = choice.manifest;
    142             const title = document.createElement('strong');
    143             title.textContent = (choice.definition ? choice.definition + ' · ' : '') + choice.title + (choice.library ? ' · ' + choice.library.version : '');
    144             const description = document.createElement('span');
    145             description.textContent = choice.description;
    146             const label = document.createElement('small');
    147             const match = choice.names.filter(n => search.value.trim() && n.toLowerCase().includes(search.value.toLowerCase().trim())).slice(0, 5);
    148             label.textContent = `${choice.library ? 'Published library' : 'Local file'} · ${choice.names.length} exports${match.length ? ' · ' + match.join(', ') : ''}`;
    149             button.append(title, description, label);
    150             button.onclick = () => show(choice);
    151             results.append(button);
    152         };
    153         heading('Libraries');
    154         for (const choice of choices)
    155             addChoice(choice);
    156         if (search.value.trim()) {
    157             heading('Definitions');
    158             const query = search.value.toLowerCase().trim();
    159             const matches = choices.flatMap(choice => choice.names.filter(name => name.toLowerCase().includes(query)).map(definition => ({ ...choice, definition })));
    160             matches.sort((a, b) => Number(b.definition.toLowerCase() === query) - Number(a.definition.toLowerCase() === query) || a.definition.localeCompare(b.definition));
    161             for (const match of matches.slice(0, 80))
    162                 addChoice(match);
    163             if (matches.length > 80) {
    164                 const note = document.createElement('p');
    165                 note.textContent = 'Showing 80 definitions. Refine your search for more specific results.';
    166                 results.append(note);
    167             }
    168             if (!matches.length) {
    169                 const note = document.createElement('p');
    170                 note.textContent = 'No matching definitions.';
    171                 results.append(note);
    172             }
    173         }
    174         if (!choices.length)
    175             results.textContent = 'No matching libraries. Try an exported name or a broader search.';
    176     }
    177     open.onclick = () => {
    178         if (!options.available())
    179             return;
    180         snapshot = options.source.getSource();
    181         backend = options.backend();
    182         search.value = '';
    183         dialog.showModal();
    184         browse();
    185         search.focus();
    186         if (!libraries) {
    187             message('Loading library references…');
    188             void loadLibraries().then(rows => {
    189                 libraries = rows;
    190                 if (dialog.open && !selected)
    191                     browse();
    192             }).catch(error => { if (dialog.open)
    193                 message(`Library references could not be loaded: ${String(error)}. Close and reopen to retry. Local modules remain available.`); });
    194         }
    195     };
    196     search.oninput = browse;
    197     alias.oninput = check;
    198     get('import-export-search').oninput = renderExports;
    199     versions.onchange = () => { const row = libraries?.find(r => r.name === selected?.library?.name && r.version === versions.value); if (row)
    200         show({ title: row.title, description: row.description, manifest: row.manifest, names: row.names, alias: alias.value, library: row, ...(selected?.definition && row.names.includes(selected.definition) ? { definition: selected.definition } : {}) }, false); };
    201     get('import-back').onclick = () => { browse(); search.focus(); };
    202     get('import-cancel').onclick = finish;
    203     dialog.addEventListener('cancel', () => stop());
    204     dialog.addEventListener('close', () => { if (!dialog.open)
    205         stop(); });
    206     tryButton.onclick = () => {
    207         if (selected?.library) {
    208             const choice = selected;
    209             finish();
    210             options.tryLibrary(choice.library, choice.definition);
    211         }
    212     };
    213     const qualifiedName = (name) => {
    214         const namespace = plan?.existing?.alias ?? alias.value;
    215         return namespace === '*' ? name : `${namespace}.${name}`;
    216     };
    217     get('copy-qualified').onclick = () => {
    218         if (!selected?.definition)
    219             return;
    220         void navigator.clipboard.writeText(qualifiedName(selected.definition)).then(() => message('Qualified name copied.'), () => message('Clipboard unavailable. Use the namespace and definition shown above.'));
    221     };
    222     get('copy-import').onclick = () => { void navigator.clipboard.writeText(statement()).then(() => message('Import copied.'), () => message('Select and copy the import preview.')); };
    223     function applyPlan() {
    224         if (!plan || !options.available() || options.source.getSource() !== snapshot || options.backend() !== backend) {
    225             stop();
    226             insert.disabled = true;
    227             message('The editor changed. Close and reopen the picker to use the current source.');
    228             return;
    229         }
    230         const checked = plan;
    231         finish();
    232         options.source.focus();
    233         if (checked.existing) {
    234             options.source.select(checked.existing.start, checked.existing.end);
    235             return;
    236         }
    237         options.source.edit(0, 0, checked.statement);
    238         options.source.select(0, checked.statement.length);
    239     }
    240     insert.onclick = () => applyPlan();
    241     return { cancel: () => { inspector.hide(); if (dialog.open)
    242             finish(); } };
    243 }