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 });