mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			245 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
 | 
						|
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
 | 
						|
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter, codeFolding } from "@codemirror/language";
 | 
						|
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
 | 
						|
import { highlightSelectionMatches } from "@codemirror/search";
 | 
						|
import { vim } from "@replit/codemirror-vim";
 | 
						|
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
 | 
						|
import byMimeType from "./syntax_highlighting.js";
 | 
						|
import smartIndentWithTab from "./extensions/custom_tab.js";
 | 
						|
import type { ThemeDefinition } from "./color_themes.js";
 | 
						|
import { createSearchHighlighter, SearchHighlighter, searchMatchHighlightTheme } from "./find_replace.js";
 | 
						|
 | 
						|
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
 | 
						|
 | 
						|
type ContentChangedListener = () => void;
 | 
						|
 | 
						|
export interface EditorConfig {
 | 
						|
    parent: HTMLElement;
 | 
						|
    placeholder?: string;
 | 
						|
    lineWrapping?: boolean;
 | 
						|
    vimKeybindings?: boolean;
 | 
						|
    readOnly?: boolean;
 | 
						|
    /** Disables some of the nice-to-have features (bracket matching, syntax highlighting, indentation markers) in order to improve performance. */
 | 
						|
    preferPerformance?: boolean;
 | 
						|
    tabIndex?: number;
 | 
						|
    onContentChanged?: ContentChangedListener;
 | 
						|
}
 | 
						|
 | 
						|
export default class CodeMirror extends EditorView {
 | 
						|
 | 
						|
    private config: EditorConfig;
 | 
						|
    private languageCompartment: Compartment;
 | 
						|
    private historyCompartment: Compartment;
 | 
						|
    private themeCompartment: Compartment;
 | 
						|
    private lineWrappingCompartment: Compartment;
 | 
						|
    private searchHighlightCompartment: Compartment;
 | 
						|
    private searchPlugin?: SearchHighlighter | null;
 | 
						|
 | 
						|
    constructor(config: EditorConfig) {
 | 
						|
        const languageCompartment = new Compartment();
 | 
						|
        const historyCompartment = new Compartment();
 | 
						|
        const themeCompartment = new Compartment();
 | 
						|
        const lineWrappingCompartment = new Compartment();
 | 
						|
        const searchHighlightCompartment = new Compartment();
 | 
						|
 | 
						|
        let extensions: Extension[] = [];
 | 
						|
 | 
						|
        if (config.vimKeybindings) {
 | 
						|
            extensions.push(vim());
 | 
						|
        }
 | 
						|
 | 
						|
        extensions = [
 | 
						|
            ...extensions,
 | 
						|
            languageCompartment.of([]),
 | 
						|
            lineWrappingCompartment.of(config.lineWrapping ? EditorView.lineWrapping : []),
 | 
						|
            searchMatchHighlightTheme,
 | 
						|
            searchHighlightCompartment.of([]),
 | 
						|
            highlightActiveLine(),
 | 
						|
            lineNumbers(),
 | 
						|
            indentUnit.of(" ".repeat(4)),
 | 
						|
            keymap.of([
 | 
						|
                ...defaultKeymap,
 | 
						|
                ...historyKeymap,
 | 
						|
                ...smartIndentWithTab
 | 
						|
            ])
 | 
						|
        ]
 | 
						|
 | 
						|
        if (!config.preferPerformance) {
 | 
						|
            extensions = [
 | 
						|
                ...extensions,
 | 
						|
                themeCompartment.of([
 | 
						|
                    syntaxHighlighting(defaultHighlightStyle, { fallback: true })
 | 
						|
                ]),
 | 
						|
                highlightSelectionMatches(),
 | 
						|
                bracketMatching(),
 | 
						|
                codeFolding(),
 | 
						|
                foldGutter(),
 | 
						|
                indentationMarkers(),
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        if (!config.readOnly) {
 | 
						|
            // Logic specific to editable notes
 | 
						|
            if (config.placeholder) {
 | 
						|
                extensions.push(placeholder(config.placeholder));
 | 
						|
            }
 | 
						|
 | 
						|
            if (config.onContentChanged) {
 | 
						|
                extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v)));
 | 
						|
            }
 | 
						|
 | 
						|
            extensions.push(historyCompartment.of(history()));
 | 
						|
        } else {
 | 
						|
            // Logic specific to read-only notes
 | 
						|
            extensions.push(EditorState.readOnly.of(true));
 | 
						|
        }
 | 
						|
 | 
						|
        super({
 | 
						|
            parent: config.parent,
 | 
						|
            extensions
 | 
						|
        });
 | 
						|
 | 
						|
        if (config.tabIndex) {
 | 
						|
            this.dom.tabIndex = config.tabIndex;
 | 
						|
        }
 | 
						|
 | 
						|
        this.config = config;
 | 
						|
        this.languageCompartment = languageCompartment;
 | 
						|
        this.historyCompartment = historyCompartment;
 | 
						|
        this.themeCompartment = themeCompartment;
 | 
						|
        this.lineWrappingCompartment = lineWrappingCompartment;
 | 
						|
        this.searchHighlightCompartment = searchHighlightCompartment;
 | 
						|
    }
 | 
						|
 | 
						|
    #onDocumentUpdated(v: ViewUpdate) {
 | 
						|
        if (v.docChanged) {
 | 
						|
            this.config.onContentChanged?.();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    getText() {
 | 
						|
        return this.state.doc.toString();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the currently selected text.
 | 
						|
     *
 | 
						|
     * If there are multiple selections, all of them will be concatenated.
 | 
						|
     */
 | 
						|
    getSelectedText() {
 | 
						|
        return this.state.selection.ranges
 | 
						|
            .map((range) => this.state.sliceDoc(range.from, range.to))
 | 
						|
            .join("");
 | 
						|
    }
 | 
						|
 | 
						|
    setText(content: string) {
 | 
						|
        this.dispatch({
 | 
						|
            changes: {
 | 
						|
                from: 0,
 | 
						|
                to: this.state.doc.length,
 | 
						|
                insert: content || "",
 | 
						|
            }
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    async setTheme(theme: ThemeDefinition) {
 | 
						|
        const extension = await theme.load();
 | 
						|
        this.dispatch({
 | 
						|
            effects: [ this.themeCompartment.reconfigure([ extension ]) ]
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    setLineWrapping(wrapping: boolean) {
 | 
						|
        this.dispatch({
 | 
						|
            effects: [ this.lineWrappingCompartment.reconfigure(wrapping ? EditorView.lineWrapping : []) ]
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Clears the history of undo/redo. Generally useful when changing to a new document.
 | 
						|
     */
 | 
						|
    clearHistory() {
 | 
						|
        if (this.config.readOnly) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        this.dispatch({
 | 
						|
            effects: [ this.historyCompartment.reconfigure([]) ]
 | 
						|
        });
 | 
						|
        this.dispatch({
 | 
						|
            effects: [ this.historyCompartment.reconfigure(history())]
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    scrollToEnd() {
 | 
						|
        const endPos = this.state.doc.length;
 | 
						|
        this.dispatch({
 | 
						|
            selection: EditorSelection.cursor(endPos),
 | 
						|
            effects: EditorView.scrollIntoView(endPos, { y: "end" }),
 | 
						|
            scrollIntoView: true
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
 | 
						|
        const plugin = createSearchHighlighter();
 | 
						|
        this.dispatch({
 | 
						|
            effects: this.searchHighlightCompartment.reconfigure(plugin)
 | 
						|
        });
 | 
						|
 | 
						|
        // Wait for the plugin to activate in the next render cycle
 | 
						|
        await new Promise(requestAnimationFrame);
 | 
						|
        const instance = this.plugin(plugin);
 | 
						|
        instance?.searchFor(searchTerm, matchCase, wholeWord);
 | 
						|
        this.searchPlugin = instance;
 | 
						|
 | 
						|
        return {
 | 
						|
            totalFound: instance?.totalFound ?? 0,
 | 
						|
            currentFound: instance?.currentFound ?? 0
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    async findNext(direction: number, currentFound: number, nextFound: number) {
 | 
						|
        this.searchPlugin?.scrollToMatch(nextFound);
 | 
						|
    }
 | 
						|
 | 
						|
    async replace(replaceText: string) {
 | 
						|
        this.searchPlugin?.replaceActiveMatch(replaceText);
 | 
						|
    }
 | 
						|
 | 
						|
    async replaceAll(replaceText: string) {
 | 
						|
        this.searchPlugin?.replaceAll(replaceText);
 | 
						|
    }
 | 
						|
 | 
						|
    cleanSearch() {
 | 
						|
        if (this.searchPlugin) {
 | 
						|
            this.dispatch({
 | 
						|
                effects: this.searchHighlightCompartment.reconfigure([])
 | 
						|
            });
 | 
						|
            this.searchPlugin = null;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    async setMimeType(mime: string) {
 | 
						|
        let newExtension: Extension[] = [];
 | 
						|
 | 
						|
        const correspondingSyntax = byMimeType[mime];
 | 
						|
        if (correspondingSyntax) {
 | 
						|
            const resolvedSyntax = await correspondingSyntax();
 | 
						|
 | 
						|
            if ("token" in resolvedSyntax) {
 | 
						|
                const extension = StreamLanguage.define(resolvedSyntax);
 | 
						|
                newExtension.push(extension);
 | 
						|
            } else if (Array.isArray(resolvedSyntax)) {
 | 
						|
                newExtension = [ ...newExtension, ...resolvedSyntax ];
 | 
						|
            } else {
 | 
						|
                newExtension.push(resolvedSyntax);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        this.dispatch({
 | 
						|
            effects: this.languageCompartment.reconfigure(newExtension)
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 |