ConceptPortal-public/rsconcept/frontend/src/utils/codemirror.ts

224 lines
6.0 KiB
TypeScript
Raw Normal View History

/**
* Module: CodeMirror customizations.
*/
import { syntaxTree } from '@codemirror/language';
2025-02-22 14:04:01 +03:00
import { type NodeType, type Tree, type TreeCursor } from '@lezer/common';
import { type ReactCodeMirrorRef, type SelectionRange } from '@uiw/react-codemirror';
2023-09-27 23:36:51 +03:00
2025-04-13 23:14:00 +03:00
/** Represents syntax tree node data. */
2025-02-10 13:29:23 +03:00
interface SyntaxNode {
2023-12-28 14:04:44 +03:00
type: NodeType;
from: number;
to: number;
2023-09-27 23:36:51 +03:00
}
2025-04-13 23:14:00 +03:00
/** Represents syntax tree cursor data. */
2025-02-10 13:29:23 +03:00
interface CursorNode extends SyntaxNode {
2023-12-28 14:04:44 +03:00
isLeaf: boolean;
2023-09-27 23:36:51 +03:00
}
export function cursorNode({ type, from, to }: TreeCursor, isLeaf = false): CursorNode {
2023-12-28 14:04:44 +03:00
return { type, from, to, isLeaf };
2023-09-27 23:36:51 +03:00
}
2024-08-06 14:39:00 +03:00
interface TreeTraversalOptions {
2023-12-28 14:04:44 +03:00
beforeEnter?: (cursor: TreeCursor) => void;
onEnter: (node: CursorNode) => false | void;
onLeave?: (node: CursorNode) => false | void;
2024-08-06 14:39:00 +03:00
}
2023-09-27 23:36:51 +03:00
2025-04-13 23:14:00 +03:00
/** Implements depth-first traversal. */
function traverseTree(tree: Tree, { beforeEnter, onEnter, onLeave }: TreeTraversalOptions) {
2023-09-27 23:36:51 +03:00
const cursor = tree.cursor();
for (;;) {
2023-12-28 14:04:44 +03:00
let node = cursorNode(cursor);
let leave = false;
const enter = !node.type.isAnonymous;
if (enter && beforeEnter) beforeEnter(cursor);
node.isLeaf = !cursor.firstChild();
2023-09-27 23:36:51 +03:00
if (enter) {
2023-12-28 14:04:44 +03:00
leave = true;
if (onEnter(node) === false) return;
2023-09-27 23:36:51 +03:00
}
2023-12-28 14:04:44 +03:00
if (!node.isLeaf) continue;
2023-09-27 23:36:51 +03:00
for (;;) {
2023-12-28 14:04:44 +03:00
node = cursorNode(cursor, node.isLeaf);
2023-09-27 23:36:51 +03:00
if (leave && onLeave) if (onLeave(node) === false) return;
2023-12-28 14:04:44 +03:00
leave = cursor.type.isAnonymous;
node.isLeaf = false;
2023-09-27 23:36:51 +03:00
if (cursor.nextSibling()) break;
if (!cursor.parent()) return;
2023-12-28 14:04:44 +03:00
leave = true;
2023-09-27 23:36:51 +03:00
}
}
}
2025-04-13 23:14:00 +03:00
/** Prints tree to compact string. */
2023-09-27 23:36:51 +03:00
export function printTree(tree: Tree): string {
const state = {
output: '',
prefixes: [] as string[]
2023-12-28 14:04:44 +03:00
};
2023-09-27 23:36:51 +03:00
traverseTree(tree, {
onEnter: node => {
state.output += '[';
state.output += node.type.name;
},
onLeave: () => {
state.output += ']';
2023-12-28 14:04:44 +03:00
}
});
2023-09-27 23:36:51 +03:00
return state.output;
}
2025-04-13 23:14:00 +03:00
/** Retrieves a list of all nodes, containing given range and corresponding to a filter. */
2025-04-30 01:10:45 +03:00
export function findEnvelopingNodes(
start: number,
finish: number,
tree: Tree,
filter?: readonly number[]
): SyntaxNode[] {
2023-09-27 23:36:51 +03:00
const result: SyntaxNode[] = [];
2023-12-28 14:04:44 +03:00
tree.cursor().iterate(node => {
if ((!filter || filter.includes(node.type.id)) && node.to >= start && node.from <= finish) {
2023-09-27 23:36:51 +03:00
result.push({
2023-12-28 14:04:44 +03:00
type: node.type,
2023-09-27 23:36:51 +03:00
to: node.to,
from: node.from
});
}
});
return result;
}
2025-04-13 23:14:00 +03:00
/** Retrieves a list of all nodes, contained in given range and corresponding to a filter. */
2025-04-30 01:10:45 +03:00
export function findContainedNodes(
start: number,
finish: number,
tree: Tree,
filter?: readonly number[]
): SyntaxNode[] {
2023-09-27 23:36:51 +03:00
const result: SyntaxNode[] = [];
2023-12-28 14:04:44 +03:00
tree.cursor().iterate(node => {
if ((!filter || filter.includes(node.type.id)) && node.to <= finish && node.from >= start) {
2023-09-27 23:36:51 +03:00
result.push({
2023-12-28 14:04:44 +03:00
type: node.type,
2023-09-27 23:36:51 +03:00
to: node.to,
from: node.from
});
}
});
return result;
}
/**
* Wrapper class for CodeMirror editor.
2023-12-28 14:04:44 +03:00
*
* Assumes single range selection.
2023-12-28 14:04:44 +03:00
*/
export class CodeMirrorWrapper {
ref: Required<ReactCodeMirrorRef>;
constructor(object: Required<ReactCodeMirrorRef>) {
this.ref = object;
}
2023-09-30 17:16:20 +03:00
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;
}
2023-09-29 15:33:32 +03:00
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
}
});
}
2023-09-29 15:33:32 +03:00
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();
2023-12-28 14:04:44 +03:00
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
});
}
2023-09-29 15:33:32 +03:00
/**
2023-09-30 17:16:20 +03:00
* Access list of SyntaxNodes contained in current selection.
2023-12-28 14:04:44 +03:00
*/
2025-04-30 01:10:45 +03:00
getContainedNodes(tokenFilter?: readonly number[]): SyntaxNode[] {
2023-09-29 15:33:32 +03:00
const selection = this.getSelection();
return findContainedNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter);
}
/**
2023-09-30 17:16:20 +03:00
* Access list of SyntaxNodes enveloping current selection.
2023-12-28 14:04:44 +03:00
*/
2025-04-30 01:10:45 +03:00
getEnvelopingNodes(tokenFilter?: readonly number[]): SyntaxNode[] {
2023-09-29 15:33:32 +03:00
const selection = this.getSelection();
return findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter);
}
2023-09-30 17:16:20 +03:00
/**
* Access list of SyntaxNodes contained in documents.
2023-12-28 14:04:44 +03:00
*/
2025-04-30 01:10:45 +03:00
getAllNodes(tokenFilter?: readonly number[]): SyntaxNode[] {
2023-09-30 17:16:20 +03:00
return findContainedNodes(0, this.ref.view.state.doc.length, syntaxTree(this.ref.view.state), tokenFilter);
}
/**
* Enlarges selection to nearest spaces.
2023-12-28 14:04:44 +03:00
*
* If tokenFilter is provided then minimal valid token is selected.
2023-12-28 14:04:44 +03:00
*/
2025-04-30 01:10:45 +03:00
fixSelection(tokenFilter?: readonly 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);
}
}
2023-12-28 14:04:44 +03:00
}