tricu

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

import-folds.ts (2719B)


      1 // Hash decorations project canonical UTF-16 document ranges. Editing a revealed
      2 // hash uses ordinary character-level selection and deletion.
      3 import {EditorState, StateField, EditorSelection, EditorView, Decoration, WidgetType, type Range} from './codemirror.js';
      4 import {tokenize} from './syntax.js';
      5 export interface ImportSite {from:number;to:number;hash:string;line:number}
      6 const cachedSites=new WeakMap<EditorState['doc'],ImportSite[]>();
      7 export function importSites(state:EditorState):ImportSite[] {
      8   const cached=cachedSites.get(state.doc);if(cached)return cached;
      9   const source=state.doc.toString();
     10   const sites=tokenize(source).filter(token=>token.kind==='directive'&&token.text==='!import').flatMap(token=>{
     11     const line=state.doc.lineAt(token.start);
     12     if(source.slice(line.from,token.start).trim())return [];
     13     const match=/^[ \t]+([a-f0-9]{64})(?=[ \t\r\n]|$)/.exec(source.slice(token.end));
     14     if(!match)return [];
     15     const from=token.end+match[0].length-64;
     16     return [{from,to:from+64,hash:match[1]!,line:line.number}];
     17   });
     18   cachedSites.set(state.doc,sites);return sites;
     19 }
     20 const short=(hash:string)=>hash.slice(0,3)+'...'+hash.slice(-3);
     21 class ImportHash extends WidgetType {
     22   constructor(readonly site:ImportSite){super();}
     23   eq(other:ImportHash):boolean{return this.site.from===other.site.from&&this.site.hash===other.site.hash;}
     24   toDOM(view:EditorView):HTMLElement {
     25     const button=document.createElement('button');button.type='button';button.className='import-hash';
     26     button.textContent=short(this.site.hash);
     27     button.setAttribute('aria-label',`Place cursor at import ${this.site.hash}`);
     28     button.onclick=()=>{
     29       view.dispatch({selection:EditorSelection.single(this.site.to)});
     30       view.focus();
     31     };
     32     return button;
     33   }
     34   ignoreEvent():boolean{return true;}
     35 }
     36 export function selectedImportLines(state:EditorState):Set<number> {
     37   return new Set(state.selection.ranges.map(range=>state.doc.lineAt(range.head).number));
     38 }
     39 function decorate(state:EditorState):ReturnType<typeof Decoration.set> {
     40   const selected=selectedImportLines(state),ranges:Range<Decoration>[]=[];
     41   for(const site of importSites(state))ranges.push((selected.has(site.line)?Decoration.mark({class:'import-hash-full',attributes:{'data-import-hash':site.hash,role:'button',tabindex:'0','aria-label':'Browse imported module '+site.hash}}):
     42     Decoration.replace({widget:new ImportHash(site)})).range(site.from,site.to));
     43   return Decoration.set(ranges,true);
     44 }
     45 export const importFolds=StateField.define<ReturnType<typeof Decoration.set>>({
     46   create:decorate,
     47   update:(old,tr)=>tr.docChanged||tr.selection?decorate(tr.state):old,
     48   provide:field=>EditorView.decorations.from(field)
     49 });