mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 19:19:03 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			244 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			244 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 } 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(),
 | |
|                 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)
 | |
|         });
 | |
|     }
 | |
| }
 | 
