import { syntaxTree } from '@codemirror/language'; import type { Extension, Range } from '@codemirror/state'; import type { DecorationSet, ViewUpdate } from '@codemirror/view'; import { ViewPlugin, EditorView, Decoration, WidgetType } from '@codemirror/view'; const pathStr = ``; export interface HyperLinkState { from: number; to: number; url: string; } class HyperLink extends WidgetType { protected readonly state: HyperLinkState; constructor(state: HyperLinkState) { super(); this.state = state; } override eq(other: HyperLink) { return ( this.state.url === other.state.url && this.state.to === other.state.to && this.state.from === other.state.from ); } toDOM() { const wrapper = document.createElement('a'); wrapper.href = this.state.url; wrapper.target = '__blank'; wrapper.innerHTML = pathStr; wrapper.className = 'cm-hyper-link-icon'; return wrapper; } override ignoreEvent() { return false; } } function hyperLinkDecorations(view: EditorView) { const widgets: Range[] = []; for (const range of view.visibleRanges) { syntaxTree(view.state).iterate({ from: range.from, to: range.to, enter: ({ type, from, to }) => { const callExp: string = view.state.doc.sliceString(from, to); if (type.name === 'URL') { const widget = Decoration.widget({ widget: new HyperLink({ from, to, url: callExp, }), side: 1, }); widgets.push(widget.range(to)); } }, }); } return Decoration.set(widgets); } export function hyperLinkExtension() { return ViewPlugin.fromClass( class HyperLinkView { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = hyperLinkDecorations(view); } update(update: ViewUpdate) { if (update.docChanged || update.viewportChanged) { this.decorations = hyperLinkDecorations(update.view); } } }, { decorations: (v) => v.decorations, }, ); } export const hyperLinkStyle = EditorView.baseTheme({ '.cm-hyper-link-icon': { display: 'inline-block', verticalAlign: 'middle', marginLeft: '0.2ch', }, '.cm-hyper-link-icon svg': { display: 'block', }, }); export const hyperLink: Extension = [hyperLinkExtension(), hyperLinkStyle];