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