mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	chore(code/find): reintroduce match highlighting
This commit is contained in:
		@@ -31,106 +31,17 @@ export default class FindInCode {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
 | 
					    async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
 | 
				
			||||||
        let findResult: Match[] | null = null;
 | 
					        const totalFound = 0;
 | 
				
			||||||
        let totalFound = 0;
 | 
					        const currentFound = 0;
 | 
				
			||||||
        let currentFound = -1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // See https://codemirror.net/addon/search/searchcursor.js for tips
 | 
					 | 
				
			||||||
        const codeEditor = await this.getCodeEditor();
 | 
					        const codeEditor = await this.getCodeEditor();
 | 
				
			||||||
        if (!codeEditor) {
 | 
					        if (!codeEditor) {
 | 
				
			||||||
            return { totalFound: 0, currentFound: 0 };
 | 
					            return { totalFound, currentFound };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const doc = codeEditor.doc;
 | 
					        await codeEditor.performFind(searchTerm, matchCase, wholeWord);
 | 
				
			||||||
        const text = doc.getValue();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Clear all markers
 | 
					        return { totalFound, currentFound };
 | 
				
			||||||
        if (this.findResult) {
 | 
					 | 
				
			||||||
            codeEditor.operation(() => {
 | 
					 | 
				
			||||||
                const findResult = this.findResult as Match[];
 | 
					 | 
				
			||||||
                for (let i = 0; i < findResult.length; ++i) {
 | 
					 | 
				
			||||||
                    const marker = findResult[i];
 | 
					 | 
				
			||||||
                    marker.clear();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (searchTerm !== "") {
 | 
					 | 
				
			||||||
            searchTerm = utils.escapeRegExp(searchTerm);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Find and highlight matches
 | 
					 | 
				
			||||||
            // Find and highlight matches
 | 
					 | 
				
			||||||
            // XXX Using \\b and not using the unicode flag probably doesn't
 | 
					 | 
				
			||||||
            //     work with non-ASCII alphabets, findAndReplace uses a more
 | 
					 | 
				
			||||||
            //     complicated regexp, see
 | 
					 | 
				
			||||||
            //     https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145
 | 
					 | 
				
			||||||
            const wholeWordChar = wholeWord ? "\\b" : "";
 | 
					 | 
				
			||||||
            const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
 | 
					 | 
				
			||||||
            let curLine = 0;
 | 
					 | 
				
			||||||
            let curChar = 0;
 | 
					 | 
				
			||||||
            let curMatch: RegExpExecArray | null = null;
 | 
					 | 
				
			||||||
            findResult = [];
 | 
					 | 
				
			||||||
            // All those markText take several seconds on e.g., this ~500-line
 | 
					 | 
				
			||||||
            // script, batch them inside an operation, so they become
 | 
					 | 
				
			||||||
            // unnoticeable. Alternatively, an overlay could be used, see
 | 
					 | 
				
			||||||
            // https://codemirror.net/addon/search/match-highlighter.js ?
 | 
					 | 
				
			||||||
            codeEditor.operation(() => {
 | 
					 | 
				
			||||||
                for (let i = 0; i < text.length; ++i) {
 | 
					 | 
				
			||||||
                    // Fetch the next match if it's the first time or if past the current match start
 | 
					 | 
				
			||||||
                    if (curMatch == null || curMatch.index < i) {
 | 
					 | 
				
			||||||
                        curMatch = re.exec(text);
 | 
					 | 
				
			||||||
                        if (curMatch == null) {
 | 
					 | 
				
			||||||
                            // No more matches
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    // Create a non-selected highlight marker for the match, the
 | 
					 | 
				
			||||||
                    // selected marker highlight will be done later
 | 
					 | 
				
			||||||
                    if (i === curMatch.index) {
 | 
					 | 
				
			||||||
                        let fromPos = { line: curLine, ch: curChar };
 | 
					 | 
				
			||||||
                        // If multiline is supported, this needs to recalculate curLine since the match may span lines
 | 
					 | 
				
			||||||
                        let toPos = { line: curLine, ch: curChar + curMatch[0].length };
 | 
					 | 
				
			||||||
                        // or css = "color: #f3"
 | 
					 | 
				
			||||||
                        let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
 | 
					 | 
				
			||||||
                        findResult?.push(marker);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        // Set the first match beyond the cursor as the current match
 | 
					 | 
				
			||||||
                        if (currentFound === -1) {
 | 
					 | 
				
			||||||
                            const cursorPos = codeEditor.getCursor();
 | 
					 | 
				
			||||||
                            if (fromPos.line > cursorPos.line || (fromPos.line === cursorPos.line && fromPos.ch >= cursorPos.ch)) {
 | 
					 | 
				
			||||||
                                currentFound = totalFound;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        totalFound++;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    // Do line and char position tracking
 | 
					 | 
				
			||||||
                    if (text[i] === "\n") {
 | 
					 | 
				
			||||||
                        curLine++;
 | 
					 | 
				
			||||||
                        curChar = 0;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        curChar++;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.findResult = findResult;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Calculate curfound if not already, highlight it as selected
 | 
					 | 
				
			||||||
        if (findResult && totalFound > 0) {
 | 
					 | 
				
			||||||
            currentFound = Math.max(0, currentFound);
 | 
					 | 
				
			||||||
            let marker = findResult[currentFound];
 | 
					 | 
				
			||||||
            let pos = marker.find();
 | 
					 | 
				
			||||||
            codeEditor.scrollIntoView(pos.to);
 | 
					 | 
				
			||||||
            marker.clear();
 | 
					 | 
				
			||||||
            findResult[currentFound] = doc.markText(pos.from, pos.to, { className: FIND_RESULT_SELECTED_CSS_CLASSNAME });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            totalFound,
 | 
					 | 
				
			||||||
            currentFound: Math.min(currentFound + 1, totalFound)
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async findNext(direction: number, currentFound: number, nextFound: number) {
 | 
					    async findNext(direction: number, currentFound: number, nextFound: number) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										45
									
								
								packages/codemirror/src/find_replace.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								packages/codemirror/src/find_replace.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view";
 | 
				
			||||||
 | 
					import { StateEffect, Compartment } from "@codemirror/state";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createSearchHighlighter(view: EditorView, searchTerm: string, matchCase: boolean, wholeWord: boolean) {
 | 
				
			||||||
 | 
					    // Escape the search term for use in RegExp
 | 
				
			||||||
 | 
					    const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
 | 
				
			||||||
 | 
					    const wordBoundary = wholeWord ? "\\b" : "";
 | 
				
			||||||
 | 
					    const flags = matchCase ? "g" : "gi";
 | 
				
			||||||
 | 
					    const regex = new RegExp(`${wordBoundary}${escapedTerm}${wordBoundary}`, flags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const matcher = new MatchDecorator({
 | 
				
			||||||
 | 
					        regexp: regex,
 | 
				
			||||||
 | 
					        decoration: searchMatchDecoration,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ViewPlugin.fromClass(class SearchHighlighter {
 | 
				
			||||||
 | 
					        matches = matcher.createDeco(view);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        constructor(public view: EditorView) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        update(update: ViewUpdate) {
 | 
				
			||||||
 | 
					            if (update.docChanged || update.viewportChanged) {
 | 
				
			||||||
 | 
					                this.matches = matcher.createDeco(update.view);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        destroy() {
 | 
				
			||||||
 | 
					            // Do nothing.
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        static deco = (v: SearchHighlighter) => v.matches;
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					        decorations: v => v.matches
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const searchMatchHighlightTheme = EditorView.baseTheme({
 | 
				
			||||||
 | 
					    ".cm-searchMatch": {
 | 
				
			||||||
 | 
					        backgroundColor: "rgba(255, 255, 0, 0.4)",
 | 
				
			||||||
 | 
					        borderRadius: "2px"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -7,6 +7,7 @@ import { vim } from "@replit/codemirror-vim";
 | 
				
			|||||||
import byMimeType from "./syntax_highlighting.js";
 | 
					import byMimeType from "./syntax_highlighting.js";
 | 
				
			||||||
import smartIndentWithTab from "./extensions/custom_tab.js";
 | 
					import smartIndentWithTab from "./extensions/custom_tab.js";
 | 
				
			||||||
import type { ThemeDefinition } from "./color_themes.js";
 | 
					import type { ThemeDefinition } from "./color_themes.js";
 | 
				
			||||||
 | 
					import { createSearchHighlighter, searchMatchHighlightTheme } from "./find_replace.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
 | 
					export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,12 +30,14 @@ export default class CodeMirror extends EditorView {
 | 
				
			|||||||
    private historyCompartment: Compartment;
 | 
					    private historyCompartment: Compartment;
 | 
				
			||||||
    private themeCompartment: Compartment;
 | 
					    private themeCompartment: Compartment;
 | 
				
			||||||
    private lineWrappingCompartment: Compartment;
 | 
					    private lineWrappingCompartment: Compartment;
 | 
				
			||||||
 | 
					    private searchHighlightCompartment: Compartment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(config: EditorConfig) {
 | 
					    constructor(config: EditorConfig) {
 | 
				
			||||||
        const languageCompartment = new Compartment();
 | 
					        const languageCompartment = new Compartment();
 | 
				
			||||||
        const historyCompartment = new Compartment();
 | 
					        const historyCompartment = new Compartment();
 | 
				
			||||||
        const themeCompartment = new Compartment();
 | 
					        const themeCompartment = new Compartment();
 | 
				
			||||||
        const lineWrappingCompartment = new Compartment();
 | 
					        const lineWrappingCompartment = new Compartment();
 | 
				
			||||||
 | 
					        const searchHighlightCompartment = new Compartment();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let extensions: Extension[] = [];
 | 
					        let extensions: Extension[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,6 +52,10 @@ export default class CodeMirror extends EditorView {
 | 
				
			|||||||
            themeCompartment.of([
 | 
					            themeCompartment.of([
 | 
				
			||||||
                syntaxHighlighting(defaultHighlightStyle, { fallback: true })
 | 
					                syntaxHighlighting(defaultHighlightStyle, { fallback: true })
 | 
				
			||||||
            ]),
 | 
					            ]),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            searchMatchHighlightTheme,
 | 
				
			||||||
 | 
					            searchHighlightCompartment.of([]),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            highlightActiveLine(),
 | 
					            highlightActiveLine(),
 | 
				
			||||||
            highlightSelectionMatches(),
 | 
					            highlightSelectionMatches(),
 | 
				
			||||||
            bracketMatching(),
 | 
					            bracketMatching(),
 | 
				
			||||||
@@ -92,6 +99,7 @@ export default class CodeMirror extends EditorView {
 | 
				
			|||||||
        this.historyCompartment = historyCompartment;
 | 
					        this.historyCompartment = historyCompartment;
 | 
				
			||||||
        this.themeCompartment = themeCompartment;
 | 
					        this.themeCompartment = themeCompartment;
 | 
				
			||||||
        this.lineWrappingCompartment = lineWrappingCompartment;
 | 
					        this.lineWrappingCompartment = lineWrappingCompartment;
 | 
				
			||||||
 | 
					        this.searchHighlightCompartment = searchHighlightCompartment;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #onDocumentUpdated(v: ViewUpdate) {
 | 
					    #onDocumentUpdated(v: ViewUpdate) {
 | 
				
			||||||
@@ -163,6 +171,13 @@ export default class CodeMirror extends EditorView {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
 | 
				
			||||||
 | 
					        const plugin = createSearchHighlighter(this, searchTerm, matchCase, wholeWord);
 | 
				
			||||||
 | 
					        this.dispatch({
 | 
				
			||||||
 | 
					            effects: this.searchHighlightCompartment.reconfigure(plugin)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async setMimeType(mime: string) {
 | 
					    async setMimeType(mime: string) {
 | 
				
			||||||
        let newExtension: Extension[] = [];
 | 
					        let newExtension: Extension[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user