mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	feat(quick_search): also allow for the equals operator in note title's quick search (#6769)
This commit is contained in:
		| @@ -59,6 +59,34 @@ describe("Lexer fulltext", () => { | ||||
|     it("escaping special characters", () => { | ||||
|         expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "#~'"]); | ||||
|     }); | ||||
|  | ||||
|     it("recognizes leading = operator for exact match", () => { | ||||
|         const result1 = lex("=example"); | ||||
|         expect(result1.fulltextTokens.map((t) => t.token)).toEqual(["example"]); | ||||
|         expect(result1.leadingOperator).toBe("="); | ||||
|  | ||||
|         const result2 = lex("=hello world"); | ||||
|         expect(result2.fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]); | ||||
|         expect(result2.leadingOperator).toBe("="); | ||||
|  | ||||
|         const result3 = lex("='hello world'"); | ||||
|         expect(result3.fulltextTokens.map((t) => t.token)).toEqual(["hello world"]); | ||||
|         expect(result3.leadingOperator).toBe("="); | ||||
|     }); | ||||
|  | ||||
|     it("doesn't treat = as leading operator in other contexts", () => { | ||||
|         const result1 = lex("==example"); | ||||
|         expect(result1.fulltextTokens.map((t) => t.token)).toEqual(["==example"]); | ||||
|         expect(result1.leadingOperator).toBe(""); | ||||
|  | ||||
|         const result2 = lex("= example"); | ||||
|         expect(result2.fulltextTokens.map((t) => t.token)).toEqual(["=", "example"]); | ||||
|         expect(result2.leadingOperator).toBe(""); | ||||
|  | ||||
|         const result3 = lex("example"); | ||||
|         expect(result3.fulltextTokens.map((t) => t.token)).toEqual(["example"]); | ||||
|         expect(result3.leadingOperator).toBe(""); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe("Lexer expression", () => { | ||||
|   | ||||
| @@ -10,10 +10,18 @@ function lex(str: string) { | ||||
|     let quotes: boolean | string = false; // otherwise contains used quote - ', " or ` | ||||
|     let fulltextEnded = false; | ||||
|     let currentWord = ""; | ||||
|     let leadingOperator = ""; | ||||
|  | ||||
|     function isSymbolAnOperator(chr: string) { | ||||
|         return ["=", "*", ">", "<", "!", "-", "+", "%", ","].includes(chr); | ||||
|     } | ||||
|      | ||||
|     // Check if the string starts with an exact match operator | ||||
|     // This allows users to use "=searchterm" for exact matching | ||||
|     if (str.startsWith("=") && str.length > 1 && str[1] !== "=" && str[1] !== " ") { | ||||
|         leadingOperator = "="; | ||||
|         str = str.substring(1); // Remove the leading operator from the string | ||||
|     } | ||||
|  | ||||
|     function isPreviousSymbolAnOperator() { | ||||
|         if (currentWord.length === 0) { | ||||
| @@ -128,7 +136,8 @@ function lex(str: string) { | ||||
|     return { | ||||
|         fulltextQuery, | ||||
|         fulltextTokens, | ||||
|         expressionTokens | ||||
|         expressionTokens, | ||||
|         leadingOperator | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import type SearchContext from "../search_context.js"; | ||||
| import type { TokenData, TokenStructure } from "./types.js"; | ||||
| import type Expression from "../expressions/expression.js"; | ||||
|  | ||||
| function getFulltext(_tokens: TokenData[], searchContext: SearchContext) { | ||||
| function getFulltext(_tokens: TokenData[], searchContext: SearchContext, leadingOperator?: string) { | ||||
|     const tokens: string[] = _tokens.map((t) => removeDiacritic(t.token)); | ||||
|  | ||||
|     searchContext.highlightedTokens.push(...tokens); | ||||
| @@ -33,8 +33,19 @@ function getFulltext(_tokens: TokenData[], searchContext: SearchContext) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     // If user specified "=" at the beginning, they want exact match | ||||
|     const operator = leadingOperator === "=" ? "=" : "*=*"; | ||||
|  | ||||
|     if (!searchContext.fastSearch) { | ||||
|         return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp("*=*", { tokens, flatText: true })]); | ||||
|         // For exact match with "=", we need different behavior | ||||
|         if (leadingOperator === "=" && tokens.length === 1) { | ||||
|             // Exact match on title OR exact match on content | ||||
|             return new OrExp([ | ||||
|                 new PropertyComparisonExp(searchContext, "title", "=", tokens[0]), | ||||
|                 new NoteContentFulltextExp("=", { tokens, flatText: false }) | ||||
|             ]); | ||||
|         } | ||||
|         return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp(operator, { tokens, flatText: true })]); | ||||
|     } else { | ||||
|         return new NoteFlatTextExp(tokens); | ||||
|     } | ||||
| @@ -428,9 +439,10 @@ export interface ParseOpts { | ||||
|     expressionTokens: TokenStructure; | ||||
|     searchContext: SearchContext; | ||||
|     originalQuery?: string; | ||||
|     leadingOperator?: string; | ||||
| } | ||||
|  | ||||
| function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) { | ||||
| function parse({ fulltextTokens, expressionTokens, searchContext, leadingOperator }: ParseOpts) { | ||||
|     let expression: Expression | undefined | null; | ||||
|  | ||||
|     try { | ||||
| @@ -444,7 +456,7 @@ function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) { | ||||
|     let exp = AndExp.of([ | ||||
|         searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", "=", "false"), | ||||
|         getAncestorExp(searchContext), | ||||
|         getFulltext(fulltextTokens, searchContext), | ||||
|         getFulltext(fulltextTokens, searchContext, leadingOperator), | ||||
|         expression | ||||
|     ]); | ||||
|  | ||||
|   | ||||
| @@ -234,6 +234,28 @@ describe("Search", () => { | ||||
|         expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("leading = operator for exact match", () => { | ||||
|         rootNote | ||||
|             .child(note("Example Note").label("type", "document")) | ||||
|             .child(note("Examples of Usage").label("type", "tutorial")) | ||||
|             .child(note("Sample").label("type", "example")); | ||||
|  | ||||
|         const searchContext = new SearchContext(); | ||||
|  | ||||
|         // Using leading = for exact title match | ||||
|         let searchResults = searchService.findResultsWithQuery("=Example Note", searchContext); | ||||
|         expect(searchResults.length).toEqual(1); | ||||
|         expect(findNoteByTitle(searchResults, "Example Note")).toBeTruthy(); | ||||
|  | ||||
|         // Without =, it should find all notes containing "example" | ||||
|         searchResults = searchService.findResultsWithQuery("example", searchContext); | ||||
|         expect(searchResults.length).toEqual(3); | ||||
|  | ||||
|         // = operator should not match partial words | ||||
|         searchResults = searchService.findResultsWithQuery("=Example", searchContext); | ||||
|         expect(searchResults.length).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     it("fuzzy attribute search", () => { | ||||
|         rootNote.child(note("Europe") | ||||
|                 .label("country", "", true) | ||||
|   | ||||
| @@ -367,7 +367,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S | ||||
| } | ||||
|  | ||||
| function parseQueryToExpression(query: string, searchContext: SearchContext) { | ||||
|     const { fulltextQuery, fulltextTokens, expressionTokens } = lex(query); | ||||
|     const { fulltextQuery, fulltextTokens, expressionTokens, leadingOperator } = lex(query); | ||||
|     searchContext.fulltextQuery = fulltextQuery; | ||||
|  | ||||
|     let structuredExpressionTokens: TokenStructure; | ||||
| @@ -383,7 +383,8 @@ function parseQueryToExpression(query: string, searchContext: SearchContext) { | ||||
|         fulltextTokens, | ||||
|         expressionTokens: structuredExpressionTokens, | ||||
|         searchContext, | ||||
|         originalQuery: query | ||||
|         originalQuery: query, | ||||
|         leadingOperator | ||||
|     }); | ||||
|  | ||||
|     if (searchContext.debug) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user