/** * Module: CodeMirror customizations. */ import { syntaxTree } from '@codemirror/language'; import { NodeType, Tree, TreeCursor } from '@lezer/common'; import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'; /** * Represents syntax tree node data. */ interface SyntaxNode { type: NodeType; from: number; to: number; } /** * Represents syntax tree cursor data. */ interface CursorNode extends SyntaxNode { isLeaf: boolean; } export function cursorNode({ type, from, to }: TreeCursor, isLeaf = false): CursorNode { return { type, from, to, isLeaf }; } interface TreeTraversalOptions { beforeEnter?: (cursor: TreeCursor) => void; onEnter: (node: CursorNode) => false | void; onLeave?: (node: CursorNode) => false | void; } /** * Implements depth-first traversal. */ export function traverseTree(tree: Tree, { beforeEnter, onEnter, onLeave }: TreeTraversalOptions) { const cursor = tree.cursor(); for (;;) { let node = cursorNode(cursor); let leave = false; const enter = !node.type.isAnonymous; if (enter && beforeEnter) beforeEnter(cursor); node.isLeaf = !cursor.firstChild(); if (enter) { leave = true; if (onEnter(node) === false) return; } if (!node.isLeaf) continue; for (;;) { node = cursorNode(cursor, node.isLeaf); if (leave && onLeave) if (onLeave(node) === false) return; leave = cursor.type.isAnonymous; node.isLeaf = false; if (cursor.nextSibling()) break; if (!cursor.parent()) return; leave = true; } } } /** * Prints tree to compact string. */ export function printTree(tree: Tree): string { const state = { output: '', prefixes: [] as string[] }; traverseTree(tree, { onEnter: node => { state.output += '['; state.output += node.type.name; }, onLeave: () => { state.output += ']'; } }); return state.output; } /** * Retrieves a list of all nodes, containing given range and corresponding to a filter. */ export function findEnvelopingNodes(start: number, finish: number, tree: Tree, filter?: number[]): SyntaxNode[] { const result: SyntaxNode[] = []; tree.cursor().iterate(node => { if ((!filter || filter.includes(node.type.id)) && node.to >= start && node.from <= finish) { result.push({ type: node.type, to: node.to, from: node.from }); } }); return result; } /** * Retrieves a list of all nodes, contained in given range and corresponding to a filter. */ export function findContainedNodes(start: number, finish: number, tree: Tree, filter?: number[]): SyntaxNode[] { const result: SyntaxNode[] = []; tree.cursor().iterate(node => { if ((!filter || filter.includes(node.type.id)) && node.to <= finish && node.from >= start) { result.push({ type: node.type, to: node.to, from: node.from }); } }); return result; } /** * Wrapper class for CodeMirror editor. * * Assumes single range selection. */ export class CodeMirrorWrapper { ref: Required; constructor(object: Required) { this.ref = object; } getText(from: number, to: number): string { return this.ref.view.state.doc.sliceString(from, to); } getWord(position: number): SelectionRange | null { return this.ref.view.state.wordAt(position); } getSelection(): SelectionRange { return this.ref.view.state.selection.main; } getSelectionText(): string { const selection = this.getSelection(); return this.ref.view.state.doc.sliceString(selection.from, selection.to); } setSelection(from: number, to: number) { this.ref.view.dispatch({ selection: { anchor: from, head: to } }); } insertChar(key: string) { this.replaceWith(key); } replaceWith(data: string) { this.ref.view.dispatch(this.ref.view.state.replaceSelection(data)); } envelopeWith(left: string, right: string) { const selection = this.getSelection(); const newSelection = !selection.empty ? { anchor: selection.from, head: selection.to + left.length + right.length } : { anchor: selection.to + left.length + right.length - 1 }; this.ref.view.dispatch({ changes: [ { from: selection.from, insert: left }, { from: selection.to, insert: right } ], selection: newSelection }); } /** * Access list of SyntaxNodes contained in current selection. */ getContainedNodes(tokenFilter?: number[]): SyntaxNode[] { const selection = this.getSelection(); return findContainedNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter); } /** * Access list of SyntaxNodes enveloping current selection. */ getEnvelopingNodes(tokenFilter?: number[]): SyntaxNode[] { const selection = this.getSelection(); return findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter); } /** * Access list of SyntaxNodes contained in documents. */ getAllNodes(tokenFilter?: number[]): SyntaxNode[] { return findContainedNodes(0, this.ref.view.state.doc.length, syntaxTree(this.ref.view.state), tokenFilter); } /** * Enlarges selection to nearest spaces. * * If tokenFilter is provided then minimal valid token is selected. */ fixSelection(tokenFilter?: number[]) { const selection = this.getSelection(); if (tokenFilter) { const nodes = findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter); if (nodes.length > 0) { const target = nodes[nodes.length - 1]; this.setSelection(target.from, target.to); return; } } const startWord = this.ref.view.state.wordAt(selection.from); const endWord = this.ref.view.state.wordAt(selection.to); if (startWord || endWord) { this.setSelection(startWord?.from ?? selection.from, endWord?.to ?? selection.to); } } }