From 1b442d1f245b56a4a726d130b7566e22fa0b5cd7 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Wed, 25 Sep 2024 20:47:27 +0200 Subject: [PATCH 01/19] feat(#245): in results list, show where an image/pdf is embedded * Add an index of embeds for reference in results * Notes embedding images are now shown in results * Updated dependencies * Correctly referencing all embeds * Updated docs * Basic embedded feature ok --- Doc Omnisearch/Index.md | 9 ++- assets/styles.css | 7 +- manifest.json | 2 +- package.json | 4 +- pnpm-lock.yaml | 90 +++++++++++++---------- src/cache-manager.ts | 23 ++++-- src/components/ResultItemContainer.svelte | 3 +- src/components/ResultItemVault.svelte | 76 ++++++++++++------- src/database.ts | 18 ++--- src/globals.ts | 1 + src/main.ts | 11 ++- src/repositories/embeds-repository.ts | 77 +++++++++++++++++++ src/search/search-engine.ts | 58 ++++++++++++--- src/tools/utils.ts | 2 +- 14 files changed, 280 insertions(+), 101 deletions(-) create mode 100644 src/repositories/embeds-repository.ts diff --git a/Doc Omnisearch/Index.md b/Doc Omnisearch/Index.md index a053f96..71e1a22 100644 --- a/Doc Omnisearch/Index.md +++ b/Doc Omnisearch/Index.md @@ -5,8 +5,8 @@ - Omnisearch is available on [the official Community Plugins repository](https://obsidian.md/plugins?search=Omnisearch). - Beta releases can be installed through [BRAT](https://github.com/TfTHacker/obsidian42-brat). **Be advised that those versions can be buggy and break things.** -> [!INFO] Chinese, Japanese, Korean, ... -> If you have notes in a CJK language, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) +> [!INFO] Chinese users +> If you have notes in Chinese, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) for better search results. ## Documentation @@ -22,8 +22,9 @@ Omnisearch is licensed under [GPL-3](https://tldrlegal.com/license/gnu-general-p ## Thanks -❤ To all people who donate through [Ko-Fi](https://ko-fi.com/scambier)or [Github Sponsors](https://github.com/sponsors/scambier), to code contributors, and to Obsidian who graciously provides this Publish space ❤ +❤ To all people who donate through [Ko-Fi](https://ko-fi.com/scambier) or [Github Sponsors](https://github.com/sponsors/scambier), to code contributors, and to the Obsidian team who graciously provides this Publish space ❤ If you wish to get involved in Omnisearch's development, there are [open issues](https://github.com/scambier/obsidian-omnisearch/issues) that need to be solved, and probably several of them tagged as "[good first issue](https://github.com/scambier/obsidian-omnisearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)" :) -![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg) + +![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg) \ No newline at end of file diff --git a/assets/styles.css b/assets/styles.css index bdd10e7..9730110 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -5,7 +5,7 @@ white-space: normal; display: flex; flex-direction: row; - justify-content: space-between; + /* justify-content: space-between; */ flex-wrap: nowrap; } @@ -59,6 +59,11 @@ margin-inline-start: 0.5em; } +.omnisearch-result__embed { + margin-left: 1em; +} + + .omnisearch-result__image-container { flex-basis: 20%; text-align: end; diff --git a/manifest.json b/manifest.json index c3d4ce2..4fa93ed 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "omnisearch", "name": "Omnisearch", "version": "1.24.1", - "minAppVersion": "1.3.0", + "minAppVersion": "1.7.2", "description": "A search engine that just works", "author": "Simon Cambier", "authorUrl": "https://github.com/scambier/obsidian-omnisearch", diff --git a/package.json b/package.json index 5c0cb98..6532329 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "esbuild-plugin-copy": "1.3.0", "esbuild-svelte": "0.7.1", "jest": "^27.5.1", - "obsidian": "1.5.7-1", + "obsidian": "1.7.2", "prettier": "^2.8.8", "prettier-plugin-svelte": "^2.10.1", "svelte": "^3.59.2", @@ -37,7 +37,7 @@ "svelte-preprocess": "^4.10.7", "tslib": "2.3.1", "typescript": "^4.9.5", - "vite": "^3.2.10" + "vite": "^3.2.11" }, "dependencies": { "cancelable-promise": "^4.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c43382..3b32fcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,8 +76,8 @@ importers: specifier: ^27.5.1 version: 27.5.1 obsidian: - specifier: 1.5.7-1 - version: 1.5.7-1(@codemirror/state@6.4.1)(@codemirror/view@6.26.3) + specifier: 1.7.2 + version: 1.7.2(@codemirror/state@6.4.1)(@codemirror/view@6.26.3) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -89,13 +89,13 @@ importers: version: 3.59.2 svelte-check: specifier: ^2.10.3 - version: 2.10.3(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.78.0)(svelte@3.59.2) + version: 2.10.3(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.79.3)(svelte@3.59.2) svelte-jester: specifier: ^2.3.2 version: 2.3.2(jest@27.5.1)(svelte@3.59.2) svelte-preprocess: specifier: ^4.10.7 - version: 4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.78.0)(svelte@3.59.2)(typescript@4.9.5) + version: 4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.79.3)(svelte@3.59.2)(typescript@4.9.5) tslib: specifier: 2.3.1 version: 2.3.1 @@ -103,8 +103,8 @@ importers: specifier: ^4.9.5 version: 4.9.5 vite: - specifier: ^3.2.10 - version: 3.2.10(@types/node@16.18.108)(sass@1.78.0) + specifier: ^3.2.11 + version: 3.2.11(@types/node@16.18.108)(sass@1.79.3) packages: @@ -861,8 +861,8 @@ packages: '@types/codemirror@5.60.8': resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -968,8 +968,8 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} array-union@2.1.0: @@ -1072,8 +1072,8 @@ packages: cancelable-promise@4.3.1: resolution: {integrity: sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==} - caniuse-lite@1.0.30001660: - resolution: {integrity: sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==} + caniuse-lite@1.0.30001662: + resolution: {integrity: sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -1102,6 +1102,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.0: + resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1240,8 +1244,8 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - electron-to-chromium@1.5.23: - resolution: {integrity: sha512-mBhODedOXg4v5QWwl21DjM5amzjmI1zw9EPrPK/5Wx7C8jt33bpZNrC7OhHUG3pxRtbLpr3W2dXT+Ph1SsfRZA==} + electron-to-chromium@1.5.27: + resolution: {integrity: sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==} emittery@0.8.1: resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} @@ -2044,8 +2048,8 @@ packages: nwsapi@2.2.12: resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} - obsidian@1.5.7-1: - resolution: {integrity: sha512-T5ZRuQ1FnfXqEoakTTHVDYvzUEEoT8zSPnQCW31PVgYwG4D4tZCQfKHN2hTz1ifnCe8upvwa6mBTAP2WUA5Vng==} + obsidian@1.7.2: + resolution: {integrity: sha512-k9hN9brdknJC+afKr5FQzDRuEFGDKbDjfCazJwpgibwCAoZNYHYV8p/s3mM8I6AsnKrPKNXf8xGuMZ4enWelZQ==} peerDependencies: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 @@ -2169,6 +2173,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.1: + resolution: {integrity: sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==} + engines: {node: '>= 14.16.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -2253,8 +2261,8 @@ packages: sander@0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - sass@1.78.0: - resolution: {integrity: sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==} + sass@1.79.3: + resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==} engines: {node: '>=14.0.0'} hasBin: true @@ -2523,8 +2531,8 @@ packages: resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} engines: {node: '>=10.12.0'} - vite@3.2.10: - resolution: {integrity: sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==} + vite@3.2.11: + resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -3659,7 +3667,7 @@ snapshots: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.25.6 '@types/testing-library__jest-dom': 5.14.9 - aria-query: 5.3.1 + aria-query: 5.3.2 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.5.16 @@ -3695,7 +3703,7 @@ snapshots: dependencies: '@types/tern': 0.23.9 - '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} '@types/graceful-fs@4.1.9': dependencies: @@ -3732,13 +3740,13 @@ snapshots: '@types/sass@1.45.0': dependencies: - sass: 1.78.0 + sass: 1.79.3 '@types/stack-utils@2.0.3': {} '@types/tern@0.23.9': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/testing-library__jest-dom@5.14.9': dependencies: @@ -3794,7 +3802,7 @@ snapshots: dependencies: sprintf-js: 1.0.3 - aria-query@5.3.1: {} + aria-query@5.3.2: {} array-union@2.1.0: {} @@ -3899,8 +3907,8 @@ snapshots: browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001660 - electron-to-chromium: 1.5.23 + caniuse-lite: 1.0.30001662 + electron-to-chromium: 1.5.27 node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) @@ -3922,7 +3930,7 @@ snapshots: cancelable-promise@4.3.1: {} - caniuse-lite@1.0.30001660: {} + caniuse-lite@1.0.30001662: {} chalk@2.4.2: dependencies: @@ -3977,6 +3985,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.0: + dependencies: + readdirp: 4.0.1 + ci-info@3.9.0: {} cjs-module-lexer@1.4.1: {} @@ -4097,7 +4109,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - electron-to-chromium@1.5.23: {} + electron-to-chromium@1.5.27: {} emittery@0.8.1: {} @@ -5049,7 +5061,7 @@ snapshots: nwsapi@2.2.12: {} - obsidian@1.5.7-1(@codemirror/state@6.4.1)(@codemirror/view@6.26.3): + obsidian@1.7.2(@codemirror/state@6.4.1)(@codemirror/view@6.26.3): dependencies: '@codemirror/state': 6.4.1 '@codemirror/view': 6.26.3 @@ -5160,6 +5172,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.1: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -5241,9 +5255,9 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.7.1 - sass@1.78.0: + sass@1.79.3: dependencies: - chokidar: 3.6.0 + chokidar: 4.0.0 immutable: 4.3.7 source-map-js: 1.2.1 @@ -5341,7 +5355,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@2.10.3(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.78.0)(svelte@3.59.2): + svelte-check@2.10.3(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.79.3)(svelte@3.59.2): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 @@ -5350,7 +5364,7 @@ snapshots: picocolors: 1.1.0 sade: 1.8.1 svelte: 3.59.2 - svelte-preprocess: 4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.78.0)(svelte@3.59.2)(typescript@4.9.5) + svelte-preprocess: 4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.79.3)(svelte@3.59.2)(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - '@babel/core' @@ -5369,7 +5383,7 @@ snapshots: jest: 27.5.1 svelte: 3.59.2 - svelte-preprocess@4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.78.0)(svelte@3.59.2)(typescript@4.9.5): + svelte-preprocess@4.10.7(@babel/core@7.25.2)(postcss@8.4.47)(sass@1.79.3)(svelte@3.59.2)(typescript@4.9.5): dependencies: '@types/pug': 2.0.10 '@types/sass': 1.45.0 @@ -5381,7 +5395,7 @@ snapshots: optionalDependencies: '@babel/core': 7.25.2 postcss: 8.4.47 - sass: 1.78.0 + sass: 1.79.3 typescript: 4.9.5 svelte@3.59.2: {} @@ -5466,7 +5480,7 @@ snapshots: convert-source-map: 1.9.0 source-map: 0.7.4 - vite@3.2.10(@types/node@16.18.108)(sass@1.78.0): + vite@3.2.11(@types/node@16.18.108)(sass@1.79.3): dependencies: esbuild: 0.15.18 postcss: 8.4.47 @@ -5475,7 +5489,7 @@ snapshots: optionalDependencies: '@types/node': 16.18.108 fsevents: 2.3.3 - sass: 1.78.0 + sass: 1.79.3 w3c-hr-time@1.0.2: dependencies: diff --git a/src/cache-manager.ts b/src/cache-manager.ts index 5cbfb4e..60afdfd 100644 --- a/src/cache-manager.ts +++ b/src/cache-manager.ts @@ -22,7 +22,6 @@ export class CacheManager { * Show an empty input field next time the user opens Omnisearch modal */ private nextQueryIsEmpty = false - /** * The "live cache", containing all indexed vault files * in the form of IndexedDocuments @@ -45,6 +44,7 @@ export class CacheManager { return } this.documents.set(path, doc) + this.plugin.embedsRepository.refreshEmbeds(path) } catch (e) { console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e) // Shouldn't be needed, but... @@ -163,16 +163,22 @@ export class CacheManager { else if ( isFileImage(path) && ((this.plugin.settings.imagesIndexing && - extractor?.canFileBeExtracted(path)) || - (this.plugin.settings.aiImageIndexing && - aiImageAnalyzer?.canBeAnalyzed(file))) + extractor?.canFileBeExtracted(path)) || + (this.plugin.settings.aiImageIndexing && + aiImageAnalyzer?.canBeAnalyzed(file))) ) { - if (this.plugin.settings.imagesIndexing && extractor?.canFileBeExtracted(path)){ + if ( + this.plugin.settings.imagesIndexing && + extractor?.canFileBeExtracted(path) + ) { content = await extractor.extractText(file) } - if (this.plugin.settings.aiImageIndexing && aiImageAnalyzer?.canBeAnalyzed(file)) { - content = await aiImageAnalyzer.analyzeImage(file) + (content ?? '') + if ( + this.plugin.settings.aiImageIndexing && + aiImageAnalyzer?.canBeAnalyzed(file) + ) { + content = (await aiImageAnalyzer.analyzeImage(file)) + (content ?? '') } } // ** PDF ** @@ -230,7 +236,8 @@ export class CacheManager { } } } - const displayTitle = metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? '' + const displayTitle = + metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? '' const tags = getTagsFromMetadata(metadata) return { basename: file.basename, diff --git a/src/components/ResultItemContainer.svelte b/src/components/ResultItemContainer.svelte index 230b396..71cdab6 100644 --- a/src/components/ResultItemContainer.svelte +++ b/src/components/ResultItemContainer.svelte @@ -4,11 +4,12 @@ export let id: string export let selected = false export let glyph = false + export let cssClass = ''
- - {@html plugin.textProcessor.highlightText(title, matchesTitle)} + {#if note.isEmbed} + + {:else} + + {/if} + + {@html plugin.textProcessor.highlightText(title, matchesTitle)} + .{getExtension(note.path)} @@ -91,23 +107,33 @@ {#if notePath}
- {@html plugin.textProcessor.highlightText(notePath, matchesNotePath)} + + {@html plugin.textProcessor.highlightText( + notePath, + matchesNotePath + )}
{/if} -
- {#if $showExcerpt} -
- {@html plugin.textProcessor.highlightText(cleanedContent, note.matches)} -
- {/if} + + {#if !note.isEmbed} +
+ {#if $showExcerpt} +
+ {@html plugin.textProcessor.highlightText( + cleanedContent, + note.matches + )} +
+ {/if} - - {#if imagePath} -
- -
- {/if} -
+ + {#if imagePath} +
+ +
+ {/if} +
+ {/if}
diff --git a/src/database.ts b/src/database.ts index e3e9d32..834a844 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,12 +1,11 @@ import Dexie from 'dexie' -import type MiniSearch from 'minisearch' import type { AsPlainObject } from 'minisearch' import type { DocumentRef } from './globals' import { Notice } from 'obsidian' import type OmnisearchPlugin from './main' export class Database extends Dexie { - public static readonly dbVersion = 8 + public static readonly dbVersion = 9 searchHistory!: Dexie.Table<{ id?: number; query: string }, number> minisearch!: Dexie.Table< { @@ -16,6 +15,7 @@ export class Database extends Dexie { }, string > + embeds!: Dexie.Table<{ embedded: string; references: string[] }, string> constructor(private plugin: OmnisearchPlugin) { super(Database.getDbName(plugin.app.appId)) @@ -23,6 +23,7 @@ export class Database extends Dexie { this.version(Database.dbVersion).stores({ searchHistory: '++id', minisearch: 'date', + embeds: 'embedded', }) } @@ -49,17 +50,15 @@ export class Database extends Dexie { } } - public async writeMinisearchCache( - minisearch: MiniSearch, - indexed: Map - ): Promise { - const paths = Array.from(indexed).map(([k, v]) => ({ path: k, mtime: v })) + public async writeMinisearchCache(): Promise { + const minisearchJson = this.plugin.searchEngine.getSerializedMiniSearch() + const paths = this.plugin.searchEngine.getSerializedIndexedDocuments() const database = this.plugin.database await database.minisearch.clear() await database.minisearch.add({ date: new Date().toISOString(), paths, - data: minisearch.toJSON(), + data: minisearchJson, }) console.log('Omnisearch - Search cache written') } @@ -85,7 +84,8 @@ export class Database extends Dexie { } public async clearCache() { - new Notice('Omnisearch - Cache cleared. Please restart Obsidian.') await this.minisearch.clear() + await this.embeds.clear() + new Notice('Omnisearch - Cache cleared. Please restart Obsidian.') } } diff --git a/src/globals.ts b/src/globals.ts index df769f9..ab2a233 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -81,6 +81,7 @@ export type ResultNote = { content: string foundWords: string[] matches: SearchMatch[] + isEmbed: boolean } let inComposition = false diff --git a/src/main.ts b/src/main.ts index 3eeac04..59ef232 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,7 @@ import { CacheManager } from './cache-manager' import { logDebug } from './tools/utils' import { NotesIndexer } from './notes-indexer' import { TextProcessor } from './tools/text-processing' +import { EmbedsRepository } from './repositories/embeds-repository' export default class OmnisearchPlugin extends Plugin { // FIXME: fix the type @@ -42,6 +43,8 @@ export default class OmnisearchPlugin extends Plugin { public readonly textProcessor = new TextProcessor(this) public readonly searchEngine = new SearchEngine(this) + public readonly embedsRepository = new EmbedsRepository(this) + private ribbonButton?: HTMLElement private refreshIndexCallback?: () => void @@ -109,6 +112,7 @@ export default class OmnisearchPlugin extends Plugin { if (this.notesIndexer.isFileIndexable(file.path)) { logDebug('Indexing new file', file.path) searchEngine.addFromPaths([file.path]) + this.embedsRepository.refreshEmbeds(file.path) } }) ) @@ -117,6 +121,7 @@ export default class OmnisearchPlugin extends Plugin { logDebug('Removing file', file.path) this.cacheManager.removeFromLiveCache(file.path) searchEngine.removeFromPaths([file.path]) + this.embedsRepository.refreshEmbeds(file.path) }) ) this.registerEvent( @@ -124,6 +129,7 @@ export default class OmnisearchPlugin extends Plugin { if (this.notesIndexer.isFileIndexable(file.path)) { this.notesIndexer.flagNoteForReindex(file) } + this.embedsRepository.refreshEmbeds(file.path) }) ) this.registerEvent( @@ -240,7 +246,7 @@ export default class OmnisearchPlugin extends Plugin { } } - const diff = searchEngine.getDiff( + const diff = searchEngine.getDocumentsToReindex( files.map(f => ({ path: f.path, mtime: f.stat.mtime })) ) @@ -281,7 +287,8 @@ export default class OmnisearchPlugin extends Plugin { } // Write the cache - await searchEngine.writeToCache() + await this.database.writeMinisearchCache() + await this.embedsRepository.writeToCache() // Re-enable settings.caching if (cacheEnabled) { diff --git a/src/repositories/embeds-repository.ts b/src/repositories/embeds-repository.ts new file mode 100644 index 0000000..47b44db --- /dev/null +++ b/src/repositories/embeds-repository.ts @@ -0,0 +1,77 @@ +import { getLinkpath } from 'obsidian' +import type OmnisearchPlugin from '../main' +import { logDebug } from '../tools/utils' + +export class EmbedsRepository { + /** Map */ + private embeds: Map> = new Map() + + constructor(private plugin: OmnisearchPlugin) {} + + public addEmbed(embed: string, notePath: string): void { + if (!this.embeds.has(embed)) { + this.embeds.set(embed, new Set()) + } + this.embeds.get(embed)!.add(notePath) + } + + public refreshEmbeds(notePath: string): void { + this.embeds.forEach((value, key) => { + if (value.has(notePath)) { + value.delete(notePath) + } + }) + this.addEmbeds(notePath) + } + + public getEmbeds(pathEmbedded: string): string[] { + const embeds = this.embeds.has(pathEmbedded) + ? [...this.embeds.get(pathEmbedded)!] + : [] + return embeds + } + + public async writeToCache(): Promise { + logDebug('Writing embeds to cache') + const database = this.plugin.database + const data: { embedded: string; references: string[] }[] = [] + for (const [path, embedsList] of this.embeds) { + data.push({ embedded: path, references: [...embedsList] }) + } + await database.embeds.clear() + await database.embeds.bulkAdd(data) + } + + public async loadFromCache(): Promise { + const database = this.plugin.database + if (!database.embeds) { + logDebug('No embeds in cache') + return + } + logDebug('Loading embeds from cache') + const embedsArr = await database.embeds.toArray() + for (const { embedded: path, references: embeds } of embedsArr) { + for (const embed of embeds) { + this.addEmbed(path, embed) + } + } + } + + private addEmbeds(notePath: string): void { + // Get all embeds from the note + // and map them to TFiles to get the real path + const embeds = ( + this.plugin.app.metadataCache.getCache(notePath)?.embeds ?? [] + ) + .map(embed => + this.plugin.app.metadataCache.getFirstLinkpathDest( + getLinkpath(embed.link), + notePath + ) + ) + .filter(o => !!o) + for (const embed of embeds) { + this.addEmbed(embed.path, notePath) + } + } +} diff --git a/src/search/search-engine.ts b/src/search/search-engine.ts index bfaae53..3001722 100644 --- a/src/search/search-engine.ts +++ b/src/search/search-engine.ts @@ -1,4 +1,8 @@ -import MiniSearch, { type Options, type SearchResult } from 'minisearch' +import MiniSearch, { + type AsPlainObject, + type Options, + type SearchResult, +} from 'minisearch' import type { DocumentRef, IndexedDocument, ResultNote } from '../globals' import { chunkArray, logDebug, removeDiacritics } from '../tools/utils' @@ -13,6 +17,7 @@ export class SearchEngine { private minisearch: MiniSearch /** Map */ private indexedDocuments: Map = new Map() + // private previousResults: SearchResult[] = [] // private previousQuery: Query | null = null @@ -25,6 +30,7 @@ export class SearchEngine { * Return true if the cache is valid */ async loadCache(): Promise { + await this.plugin.embedsRepository.loadFromCache() const cache = await this.plugin.database.getMinisearchCache() if (cache) { this.minisearch = await MiniSearch.loadJSAsync( @@ -39,10 +45,11 @@ export class SearchEngine { } /** - * Returns the list of documents that need to be reindexed + * Returns the list of documents that need to be reindexed or removed, + * either because they are new, have been modified, or have been deleted * @param docs */ - getDiff(docs: DocumentRef[]): { + getDocumentsToReindex(docs: DocumentRef[]): { toAdd: DocumentRef[] toRemove: DocumentRef[] } { @@ -264,9 +271,9 @@ export class SearchEngine { } } - // Boost custom properties const metadata = this.plugin.app.metadataCache.getCache(path) if (metadata) { + // Boost custom properties for (const { name, weight } of settings.weightCustomProperties) { const values = metadata?.frontmatter?.[name] if (values && result.terms.some(t => values.includes(t))) { @@ -372,6 +379,28 @@ export class SearchEngine { ) ) + // Inject embeds for images, documents, and PDFs + for (let i = 0; i < documents.length; i++) { + const doc = documents[i] + const embeds = this.plugin.embedsRepository + .getEmbeds(doc.path) + // Limit to 5 embeds + .slice(0, 5) + for (const embed of embeds) { + // Inject the embed in the content after index i + documents[++i] = await this.plugin.cacheManager.getDocument(embed) + results[i] = { + id: documents[i].path, + score: 0, + terms: [], + queryTerms: [], + match: {}, + isEmbed: true, + } + // console.log(documents[i]) + } + } + // Map the raw results to get usable suggestions const resultNotes = results.map(result => { logDebug('Locating matches for', result.id) @@ -412,6 +441,7 @@ export class SearchEngine { score: result.score, foundWords, matches, + isEmbed: result.isEmbed, ...note, } return resultNote @@ -419,11 +449,21 @@ export class SearchEngine { return resultNotes } - public async writeToCache(): Promise { - await this.plugin.database.writeMinisearchCache( - this.minisearch, - this.indexedDocuments - ) + /** + * For cache saving + */ + public getSerializedMiniSearch(): AsPlainObject { + return this.minisearch.toJSON() + } + + /** + * For cache saving + */ + public getSerializedIndexedDocuments(): { path: string; mtime: number }[] { + return Array.from(this.indexedDocuments).map(([path, mtime]) => ({ + path, + mtime, + })) } private getOptions(): Options { diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 74d7beb..439962f 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -151,7 +151,7 @@ export function getCtrlKeyLabel(): 'ctrl' | '⌘' { export function isFileImage(path: string): boolean { const ext = getExtension(path) - return ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp' + return ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp' || ext === 'gif' } export function isFilePDF(path: string): boolean { From 5dec98df7cd539f96e2f6c13af11eceedbeefbc6 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Wed, 25 Sep 2024 20:49:03 +0200 Subject: [PATCH 02/19] 1.25.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6532329..317ff7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scambier.obsidian-search", - "version": "1.24.1", + "version": "1.25.0-beta.1", "description": "A search engine for Obsidian", "main": "dist/main.js", "scripts": { From 10721601b1e6cc98977b2b249d0ceaa4f1bee2a1 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Wed, 25 Sep 2024 20:52:02 +0200 Subject: [PATCH 03/19] ffs typescript. --- src/repositories/embeds-repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/embeds-repository.ts b/src/repositories/embeds-repository.ts index 47b44db..4547b92 100644 --- a/src/repositories/embeds-repository.ts +++ b/src/repositories/embeds-repository.ts @@ -71,7 +71,7 @@ export class EmbedsRepository { ) .filter(o => !!o) for (const embed of embeds) { - this.addEmbed(embed.path, notePath) + this.addEmbed(embed!.path, notePath) } } } From 983fa2120b5b91a05227373b7da9a88fd4be991f Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Fri, 27 Sep 2024 13:53:30 +0200 Subject: [PATCH 04/19] fix(#245): correctly update embeds on fs events --- src/cache-manager.ts | 2 +- src/database.ts | 4 +- src/main.ts | 9 ++-- src/repositories/embeds-repository.ts | 69 +++++++++++++++++++-------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/cache-manager.ts b/src/cache-manager.ts index 60afdfd..8f90528 100644 --- a/src/cache-manager.ts +++ b/src/cache-manager.ts @@ -44,7 +44,7 @@ export class CacheManager { return } this.documents.set(path, doc) - this.plugin.embedsRepository.refreshEmbeds(path) + this.plugin.embedsRepository.refreshEmbedsForNote(path) } catch (e) { console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e) // Shouldn't be needed, but... diff --git a/src/database.ts b/src/database.ts index 834a844..4fe1c5b 100644 --- a/src/database.ts +++ b/src/database.ts @@ -5,7 +5,7 @@ import { Notice } from 'obsidian' import type OmnisearchPlugin from './main' export class Database extends Dexie { - public static readonly dbVersion = 9 + public static readonly dbVersion = 10 searchHistory!: Dexie.Table<{ id?: number; query: string }, number> minisearch!: Dexie.Table< { @@ -15,7 +15,7 @@ export class Database extends Dexie { }, string > - embeds!: Dexie.Table<{ embedded: string; references: string[] }, string> + embeds!: Dexie.Table<{ embedded: string; referencedBy: string[] }, string> constructor(private plugin: OmnisearchPlugin) { super(Database.getDbName(plugin.app.appId)) diff --git a/src/main.ts b/src/main.ts index 59ef232..f2586d9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -112,7 +112,7 @@ export default class OmnisearchPlugin extends Plugin { if (this.notesIndexer.isFileIndexable(file.path)) { logDebug('Indexing new file', file.path) searchEngine.addFromPaths([file.path]) - this.embedsRepository.refreshEmbeds(file.path) + this.embedsRepository.refreshEmbedsForNote(file.path) } }) ) @@ -121,7 +121,7 @@ export default class OmnisearchPlugin extends Plugin { logDebug('Removing file', file.path) this.cacheManager.removeFromLiveCache(file.path) searchEngine.removeFromPaths([file.path]) - this.embedsRepository.refreshEmbeds(file.path) + this.embedsRepository.removeFile(file.path) }) ) this.registerEvent( @@ -129,7 +129,7 @@ export default class OmnisearchPlugin extends Plugin { if (this.notesIndexer.isFileIndexable(file.path)) { this.notesIndexer.flagNoteForReindex(file) } - this.embedsRepository.refreshEmbeds(file.path) + this.embedsRepository.refreshEmbedsForNote(file.path) }) ) this.registerEvent( @@ -138,8 +138,11 @@ export default class OmnisearchPlugin extends Plugin { logDebug('Renaming file', file.path) this.cacheManager.removeFromLiveCache(oldPath) await this.cacheManager.addToLiveCache(file.path) + searchEngine.removeFromPaths([oldPath]) await searchEngine.addFromPaths([file.path]) + + this.embedsRepository.renameFile(oldPath, file.path) } }) ) diff --git a/src/repositories/embeds-repository.ts b/src/repositories/embeds-repository.ts index 4547b92..7a08fdc 100644 --- a/src/repositories/embeds-repository.ts +++ b/src/repositories/embeds-repository.ts @@ -1,9 +1,9 @@ -import { getLinkpath } from 'obsidian' +import { getLinkpath, Notice } from 'obsidian' import type OmnisearchPlugin from '../main' import { logDebug } from '../tools/utils' export class EmbedsRepository { - /** Map */ + /** Map */ private embeds: Map> = new Map() constructor(private plugin: OmnisearchPlugin) {} @@ -15,13 +15,36 @@ export class EmbedsRepository { this.embeds.get(embed)!.add(notePath) } - public refreshEmbeds(notePath: string): void { - this.embeds.forEach((value, key) => { - if (value.has(notePath)) { - value.delete(notePath) + public removeFile(filePath: string): void { + // If the file is embedded + this.embeds.delete(filePath) + // If the file is a note referencing other files + this.refreshEmbedsForNote(filePath) + } + + public renameFile(oldPath: string, newPath: string): void { + // If the file is embedded + if (this.embeds.has(oldPath)) { + this.embeds.set(newPath, this.embeds.get(oldPath)!) + this.embeds.delete(oldPath) + } + // If the file is a note referencing other files + this.embeds.forEach((referencedBy, key) => { + if (referencedBy.has(oldPath)) { + referencedBy.delete(oldPath) + referencedBy.add(newPath) } }) - this.addEmbeds(notePath) + } + + public refreshEmbedsForNote(filePath: string): void { + this.embeds.forEach((referencedBy, key) => { + if (referencedBy.has(filePath)) { + referencedBy.delete(filePath) + } + }) + + this.addEmbedsForNote(filePath) } public getEmbeds(pathEmbedded: string): string[] { @@ -34,30 +57,36 @@ export class EmbedsRepository { public async writeToCache(): Promise { logDebug('Writing embeds to cache') const database = this.plugin.database - const data: { embedded: string; references: string[] }[] = [] + const data: { embedded: string; referencedBy: string[] }[] = [] for (const [path, embedsList] of this.embeds) { - data.push({ embedded: path, references: [...embedsList] }) + data.push({ embedded: path, referencedBy: [...embedsList] }) } await database.embeds.clear() await database.embeds.bulkAdd(data) } public async loadFromCache(): Promise { - const database = this.plugin.database - if (!database.embeds) { - logDebug('No embeds in cache') - return - } - logDebug('Loading embeds from cache') - const embedsArr = await database.embeds.toArray() - for (const { embedded: path, references: embeds } of embedsArr) { - for (const embed of embeds) { - this.addEmbed(path, embed) + try { + const database = this.plugin.database + if (!database.embeds) { + logDebug('No embeds in cache') + return } + logDebug('Loading embeds from cache') + const embedsArr = await database.embeds.toArray() + for (const { embedded: path, referencedBy: embeds } of embedsArr) { + for (const embed of embeds) { + this.addEmbed(path, embed) + } + } + } catch (e) { + this.plugin.database.clearCache() + console.error('Omnisearch - Error while loading embeds cache') + new Notice('Omnisearch - There was an error while loading the cache. Please restart Obsidian.') } } - private addEmbeds(notePath: string): void { + private addEmbedsForNote(notePath: string): void { // Get all embeds from the note // and map them to TFiles to get the real path const embeds = ( From 372e40fe749d5ea12ba7de7fba7d08ab2263bf0e Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Fri, 27 Sep 2024 13:53:57 +0200 Subject: [PATCH 05/19] feat(#245): added a setting to determine how many embeds references to display in results --- src/search/search-engine.ts | 2 +- src/settings.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/search/search-engine.ts b/src/search/search-engine.ts index 3001722..24d172c 100644 --- a/src/search/search-engine.ts +++ b/src/search/search-engine.ts @@ -385,7 +385,7 @@ export class SearchEngine { const embeds = this.plugin.embedsRepository .getEmbeds(doc.path) // Limit to 5 embeds - .slice(0, 5) + .slice(0, this.plugin.settings.maxEmbeds) for (const embed of embeds) { // Inject the embed in the content after index i documents[++i] = await this.plugin.cacheManager.getDocument(embed) diff --git a/src/settings.ts b/src/settings.ts index 695d8f8..7677bde 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -54,6 +54,8 @@ export interface OmnisearchSettings extends WeightingSettings { ribbonIcon: boolean /** Display the small contextual excerpt in search results */ showExcerpt: boolean + /** Number of embeds references to display in search results */ + maxEmbeds: number /** Render line returns with
in excerpts */ renderLineReturnInExcerpts: boolean /** Enable a "create note" button in the Vault Search modal */ @@ -465,6 +467,24 @@ export class SettingsTab extends PluginSettingTab { }) ) + // Show embeds + new Setting(containerEl) + .setName('Show embed references') + .setDesc( + htmlDescription(`Some results are embedded in other notes.
+ This setting controls the maximum number of embeds to show in the search results. Set to 0 to disable.
+ Also works with Text Extractor for embedded images and documents.`) + ) + .addSlider(cb => { + cb.setLimits(0, 10, 1) + .setValue(settings.maxEmbeds) + .setDynamicTooltip() + .onChange(async v => { + settings.maxEmbeds = v + await saveSettings(this.plugin) + }) + }) + // Keep line returns in excerpts new Setting(containerEl) .setName('Render line return in excerpts') @@ -791,6 +811,7 @@ export function getDefaultSettings(app: App): OmnisearchSettings { ribbonIcon: true, showExcerpt: true, + maxEmbeds: 5, renderLineReturnInExcerpts: true, showCreateButton: false, highlight: true, From dfedd374070f1ac1903a14109aa88b65de5bfc05 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Fri, 27 Sep 2024 13:54:55 +0200 Subject: [PATCH 06/19] chore: 1.25.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 317ff7f..d02204b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scambier.obsidian-search", - "version": "1.25.0-beta.1", + "version": "1.25.0-beta.2", "description": "A search engine for Obsidian", "main": "dist/main.js", "scripts": { From d82b67dfc16edfcc0bf2227136d66f31a815103d Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Fri, 27 Sep 2024 14:01:43 +0200 Subject: [PATCH 07/19] chore: updated release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d430c6e..03c0981 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: id: git-cliff with: config: cliff.toml - args: --verbose + args: -vv --latest --strip header env: GITHUB_REPO: ${{ github.repository }} From 24ee6675c4f4e6cbf25cbe963a75051bcda73f26 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Wed, 2 Oct 2024 18:21:30 +0200 Subject: [PATCH 08/19] Weights are now ranged 0-10 (instead of 0-5) --- src/settings.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 7677bde..6004151 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -781,7 +781,7 @@ export class SettingsTab extends PluginSettingTab { } weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void { - cb.setLimits(1, 5, 0.1) + cb.setLimits(1, 10, 0.5) .setValue(settings[key]) .setDynamicTooltip() .onChange(async v => { @@ -820,12 +820,12 @@ export function getDefaultSettings(app: App): OmnisearchSettings { tokenizeUrls: false, fuzziness: '1', - weightBasename: 3, - weightDirectory: 2, - weightH1: 1.5, - weightH2: 1.3, - weightH3: 1.1, - weightUnmarkedTags: 1.1, + weightBasename: 10, + weightDirectory: 7, + weightH1: 6, + weightH2: 5, + weightH3: 4, + weightUnmarkedTags: 2, weightCustomProperties: [] as { name: string; weight: number }[], httpApiEnabled: false, From a4352c365cfb6aa869643d8d945e9fa3eb7cdd34 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Tue, 8 Oct 2024 17:13:04 +0200 Subject: [PATCH 09/19] Fixed embeds injection in results --- src/cache-manager.ts | 4 +++- src/search/search-engine.ts | 30 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/cache-manager.ts b/src/cache-manager.ts index 8f90528..32b6b4d 100644 --- a/src/cache-manager.ts +++ b/src/cache-manager.ts @@ -1,4 +1,4 @@ -import { TFile } from 'obsidian' +import { normalizePath, TFile } from 'obsidian' import type { IndexedDocument } from './globals' import { extractHeadingsFromCache, @@ -49,6 +49,7 @@ export class CacheManager { console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e) // Shouldn't be needed, but... this.removeFromLiveCache(path) + // TODO: increment errors counter } } @@ -101,6 +102,7 @@ export class CacheManager { private async getAndMapIndexedDocument( path: string ): Promise { + path = normalizePath(path) const app = this.plugin.app const file = app.vault.getAbstractFileByPath(path) if (!file) throw new Error(`Invalid file path: "${path}"`) diff --git a/src/search/search-engine.ts b/src/search/search-engine.ts index 24d172c..2aa0693 100644 --- a/src/search/search-engine.ts +++ b/src/search/search-engine.ts @@ -174,7 +174,7 @@ export class SearchEngine { tokenize: text => [text], }) - logDebug('Found', results.length, 'results') + logDebug(`Found ${results.length} results`, results) // Filter query results to only keep files that match query.query.ext (if any) if (query.query.ext?.length) { @@ -295,6 +295,8 @@ export class SearchEngine { // Sort results and keep the 50 best results = results.sort((a, b) => b.score - a.score).slice(0, 50) + logDebug('Filtered results:', results) + if (results.length) logDebug('First result:', results[0]) const documents = await Promise.all( @@ -380,24 +382,29 @@ export class SearchEngine { ) // Inject embeds for images, documents, and PDFs - for (let i = 0; i < documents.length; i++) { + let total = documents.length + for (let i = 0; i < total; i++) { const doc = documents[i] + if (!doc) continue + const embeds = this.plugin.embedsRepository .getEmbeds(doc.path) - // Limit to 5 embeds .slice(0, this.plugin.settings.maxEmbeds) + + // Inject embeds in the results for (const embed of embeds) { - // Inject the embed in the content after index i - documents[++i] = await this.plugin.cacheManager.getDocument(embed) - results[i] = { - id: documents[i].path, + total++ + const newDoc = await this.plugin.cacheManager.getDocument(embed) + documents.splice(i + 1, 0, newDoc) + results.splice(i + 1, 0, { + id: newDoc.path, score: 0, terms: [], queryTerms: [], match: {}, isEmbed: true, - } - // console.log(documents[i]) + }) + i++ // Increment i to skip the newly inserted document } } @@ -436,7 +443,7 @@ export class SearchEngine { foundWords, query ) - logDebug(`Matches for ${note.basename}`, matches) + logDebug(`Matches for note "${note.path}"`, matches) const resultNote: ResultNote = { score: result.score, foundWords, @@ -446,6 +453,9 @@ export class SearchEngine { } return resultNote }) + + logDebug('Suggestions:', resultNotes) + return resultNotes } From e9faa243694e613e82bcfd0ab64f1cf53a2bbfbe Mon Sep 17 00:00:00 2001 From: acrylicus Date: Tue, 8 Oct 2024 19:27:01 +0100 Subject: [PATCH 10/19] Added support for Iconize plugin (#405) * 1.25.0-beta.1 manifest * chore: manifest 1.25.0-beta.2 * Added support for Iconize plugin * Adjusted logic to fallback to generic icon if nothing found & added supported for Lucide Icon * Added support for Emojis (thanks ChatGPT) * Added dynamic updating of icons to match search results * Moved icon logic to tools/iconUtils.ts, cleaned up ResultItemVault.svelte * Moved icon logic to tools/iconUtils.ts, cleaned up ResultItemVault.svelte * Prettified code and fixed case where CamelCase lucideicons do not render in search results * Refactored code to check for Iconize plugin enablement, rehandled errors, minor tidy ups and utilization of native obsidian functions * Minor touchups and improvements, removed unecessary error logging, consolidated LucideIcon prefix code * Null return for no iconize condition --------- Co-authored-by: Simon Cambier --- assets/styles.css | 17 +++ manifest-beta.json | 2 +- src/components/ResultItemVault.svelte | 102 ++++++++++++-- src/tools/icon-utils.ts | 187 ++++++++++++++++++++++++++ versions.json | 4 +- 5 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 src/tools/icon-utils.ts diff --git a/assets/styles.css b/assets/styles.css index 9730110..89a6322 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -87,6 +87,23 @@ gap: 5px; } +.icon { + display: inline-block; + vertical-align: middle; + width: 16px; + height: 16px; + margin-right: 4px; +} +.icon svg { + width: 100%; + height: 100%; +} +.icon-emoji { + font-size: 16px; + vertical-align: middle; + margin-right: 4px; +} + @media only screen and (max-width: 600px) { .omnisearch-input-container { flex-direction: column; diff --git a/manifest-beta.json b/manifest-beta.json index 41cad1f..69dc676 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "omnisearch", "name": "Omnisearch", - "version": "1.24.1", + "version": "1.25.0-beta.2", "minAppVersion": "1.3.0", "description": "A search engine that just works", "author": "Simon Cambier", diff --git a/src/components/ResultItemVault.svelte b/src/components/ResultItemVault.svelte index 2fa5fdc..0ee019c 100644 --- a/src/components/ResultItemVault.svelte +++ b/src/components/ResultItemVault.svelte @@ -10,9 +10,18 @@ pathWithoutFilename, } from '../tools/utils' import ResultItemContainer from './ResultItemContainer.svelte' - import { TFile, setIcon } from 'obsidian' import type OmnisearchPlugin from '../main' - import { SvelteComponent } from 'svelte' + import { setIcon, TFile } from 'obsidian' + import { onMount, SvelteComponent } from 'svelte' + + // Import icon utility functions + import { + loadIconData, + initializeIconPacks, + getIconNameForPath, + loadIconSVG, + getDefaultIconSVG, + } from '../tools/icon-utils' export let selected = false export let note: ResultNote @@ -21,6 +30,74 @@ let imagePath: string | null = null let title = '' let notePath = '' + let iconData = {} + let folderIconSVG: string | null = null + let fileIconSVG: string | null = null + let prefixToIconPack: { [prefix: string]: string } = {} + let iconsPath: string + let iconDataLoaded = false // Flag to indicate iconData is loaded + + // Initialize icon data and icon packs once when the component mounts + onMount(async () => { + iconData = await loadIconData(plugin) + const iconPacks = await initializeIconPacks(plugin) + prefixToIconPack = iconPacks.prefixToIconPack + iconsPath = iconPacks.iconsPath + iconDataLoaded = true // Set the flag after iconData is loaded + }) + + // Reactive statement to call loadIcons() whenever the note changes and iconData is loaded + $: if (note && note.path && iconDataLoaded) { + ;(async () => { + // Update title and notePath before loading icons + title = note.displayTitle || note.basename + notePath = pathWithoutFilename(note.path) + await loadIcons() + })() + } + + async function loadIcons() { + // Load folder icon + const folderIconName = getIconNameForPath(notePath, iconData) + if (folderIconName) { + folderIconSVG = await loadIconSVG( + folderIconName, + plugin, + iconsPath, + prefixToIconPack + ) + } else { + // Fallback to default folder icon + folderIconSVG = getDefaultIconSVG('folder', plugin) + } + + // Load file icon + const fileIconName = getIconNameForPath(note.path, iconData) + if (fileIconName) { + fileIconSVG = await loadIconSVG( + fileIconName, + plugin, + iconsPath, + prefixToIconPack + ) + } else { + // Fallback to default icons based on file type + fileIconSVG = getDefaultIconSVG(note.path, plugin) + } + } + + // Svelte action to render SVG content with dynamic updates + function renderSVG(node: HTMLElement, svgContent: string) { + node.innerHTML = svgContent + return { + update(newSvgContent: string) { + node.innerHTML = newSvgContent + }, + destroy() { + node.innerHTML = '' + }, + } + } let elFolderPathIcon: HTMLElement let elFilePathIcon: HTMLElement let elEmbedIcon: HTMLElement @@ -34,6 +111,7 @@ } } } + $: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords) $: matchesNotePath = plugin.textProcessor.getMatches( notePath, @@ -81,9 +159,14 @@
{#if note.isEmbed} - + {:else} - + + {#if fileIconSVG} + + {/if} {/if} {@html plugin.textProcessor.highlightText(title, matchesTitle)} @@ -106,12 +189,13 @@ {#if notePath}
- + + {#if folderIconSVG} + + {/if} - {@html plugin.textProcessor.highlightText( - notePath, - matchesNotePath - )} + {@html plugin.textProcessor.highlightText(notePath, matchesNotePath)} +
{/if} diff --git a/src/tools/icon-utils.ts b/src/tools/icon-utils.ts new file mode 100644 index 0000000..8939393 --- /dev/null +++ b/src/tools/icon-utils.ts @@ -0,0 +1,187 @@ +import { TFile, getIcon, normalizePath } from 'obsidian' +import type OmnisearchPlugin from '../main' +import { + isFileImage, + isFilePDF, + isFileCanvas, + isFileExcalidraw, + warnDebug, +} from './utils' + +export interface IconPacks { + prefixToIconPack: { [prefix: string]: string } + iconsPath: string +} + +export async function loadIconData(plugin: OmnisearchPlugin): Promise { + const app = plugin.app + + // Check if the 'obsidian-icon-folder' plugin is installed and enabled + // Casting 'app' to 'any' here to avoid TypeScript errors since 'plugins' might not be defined on 'App' + const iconFolderPlugin = (app as any).plugins.getPlugin( + 'obsidian-icon-folder' + ) + if (!iconFolderPlugin) { + return {} + } + + const dataJsonPath = `${app.vault.configDir}/plugins/obsidian-icon-folder/data.json` + try { + const dataJsonContent = await app.vault.adapter.read(dataJsonPath) + const rawIconData = JSON.parse(dataJsonContent) + // Normalize keys + const iconData: any = {} + for (const key in rawIconData) { + const normalizedKey = normalizePath(key) + iconData[normalizedKey] = rawIconData[key] + } + return iconData + } catch (e) { + warnDebug('Failed to read data.json:', e) + return {} + } +} + +export async function initializeIconPacks( + plugin: OmnisearchPlugin +): Promise { + // Add 'Li' prefix for Lucide icons + const prefixToIconPack: { [prefix: string]: string } = { Li: 'lucide-icons' } + let iconsPath = 'icons' + + const app = plugin.app + + // Access the obsidian-icon-folder plugin + const iconFolderPlugin = (app as any).plugins.getPlugin( + 'obsidian-icon-folder' + ) + + if (iconFolderPlugin) { + // Get the icons path from the plugin's settings + const iconFolderSettings = iconFolderPlugin.settings + iconsPath = iconFolderSettings?.iconPacksPath || 'icons' + const iconsDir = `${app.vault.configDir}/${iconsPath}` + + try { + const iconPackDirs = await app.vault.adapter.list(iconsDir) + if (iconPackDirs.folders && iconPackDirs.folders.length > 0) { + for (const folderPath of iconPackDirs.folders) { + const pathParts = folderPath.split('/') + const iconPackName = pathParts[pathParts.length - 1] + const prefix = createIconPackPrefix(iconPackName) + prefixToIconPack[prefix] = iconPackName + } + } + } catch (e) { + warnDebug('Failed to list icon packs:', e) + } + } + + return { prefixToIconPack, iconsPath } +} + +function createIconPackPrefix(iconPackName: string): string { + if (iconPackName.includes('-')) { + const splitted = iconPackName.split('-') + let result = splitted[0].charAt(0).toUpperCase() + for (let i = 1; i < splitted.length; i++) { + result += splitted[i].charAt(0).toLowerCase() + } + return result + } + return ( + iconPackName.charAt(0).toUpperCase() + iconPackName.charAt(1).toLowerCase() + ) +} + +export function getIconNameForPath(path: string, iconData: any): string | null { + const normalizedPath = normalizePath(path) + const iconEntry = iconData[normalizedPath] + if (iconEntry) { + if (typeof iconEntry === 'string') { + return iconEntry + } else if (typeof iconEntry === 'object' && iconEntry.iconName) { + return iconEntry.iconName + } + } + return null +} + +export function parseIconName(iconName: string): { + prefix: string + name: string +} { + const prefixMatch = iconName.match(/^[A-Z][a-z]*/) + if (prefixMatch) { + const prefix = prefixMatch[0] + const name = iconName.substring(prefix.length) + return { prefix, name } + } else { + // No prefix, treat the entire iconName as the name + return { prefix: '', name: iconName } + } +} + +export async function loadIconSVG( + iconName: string, + plugin: OmnisearchPlugin, + iconsPath: string, + prefixToIconPack: { [prefix: string]: string } +): Promise { + const parsed = parseIconName(iconName) + const { prefix, name } = parsed + + if (!prefix) { + // No prefix, assume it's an emoji or text + return `${name}` + } + + const iconPackName = prefixToIconPack[prefix] + + if (!iconPackName) { + warnDebug(`No icon pack found for prefix: ${prefix}`) + return null + } + + if (iconPackName === 'lucide-icons') { + // Convert CamelCase to dash-case for Lucide icons + const dashedName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + const iconEl = getIcon(dashedName) + if (iconEl) { + return iconEl.outerHTML + } else { + warnDebug(`Lucide icon not found: ${dashedName}`) + return null + } + } else { + if (!iconsPath) { + warnDebug('Icons path is not set. Cannot load icon SVG.') + return null + } + const iconPath = `${plugin.app.vault.configDir}/${iconsPath}/${iconPackName}/${name}.svg` + try { + const svgContent = await plugin.app.vault.adapter.read(iconPath) + return svgContent + } catch (e) { + warnDebug(`Failed to load icon SVG for ${iconName} at ${iconPath}:`, e) + return null + } + } +} + +export function getDefaultIconSVG( + notePath: string, + plugin: OmnisearchPlugin +): string { + // Return SVG content for default icons based on file type + let iconName = 'file' + if (isFileImage(notePath)) { + iconName = 'image' + } else if (isFilePDF(notePath)) { + iconName = 'file-text' + } else if (isFileCanvas(notePath) || isFileExcalidraw(notePath)) { + iconName = 'layout-dashboard' + } + const iconEl = getIcon(iconName) + return iconEl ? iconEl.outerHTML : '' +} diff --git a/versions.json b/versions.json index 890c54f..1b93930 100644 --- a/versions.json +++ b/versions.json @@ -147,5 +147,7 @@ "1.24.0-beta.2": "1.3.0", "1.24.0-beta.3": "1.3.0", "1.24.0": "1.3.0", - "1.24.1": "1.3.0" + "1.24.1": "1.3.0", + "1.25.0-beta.1": "1.3.0", + "1.25.0-beta.2": "1.3.0" } \ No newline at end of file From af63186aa2579b661a20995bc63def6e1d70ed1c Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Tue, 8 Oct 2024 20:39:25 +0200 Subject: [PATCH 11/19] CONTRIBUTING update --- CONTRIBUTING.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 461dd16..fdfec7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,9 +9,7 @@ Please read this document before beginning work on a Pull Request. ## Preface - Omnisearch is a personal hobby project. I'm happy to discuss about your ideas and additions, but ultimately it is my code to grow and maintain. -- Always file an issue/feature request before working on a PR, to make sure we're aligned and no-one is making useless work. -- Omnisearch is still in its infancy: some important features are missing, and there will be architectural changes. - - As such, I may refuse your PR simply because it will have to be refactored in a short-ish term +- ❗ Always file an issue/feature request before working on a PR, to make sure we're aligned and no-one is making useless work. ## "Good First Issue" @@ -21,7 +19,7 @@ If you wish to work on one of these issues, leave a comment and I'll assign it t ## Code guidelines -- Respect the existing style +- ❗ By default, start your fork from the `develop` branch. If the `develop` branch is behind `master`, then use `master`. When in doubt, ask :) - Don't add npm dependencies if you can avoid it. If a new dependency is unavoidable, be mindful of its size, freshness and added value. - Use Svelte for all UI needs. - Try to not shoehorn your code into existing functions or components. @@ -46,3 +44,4 @@ Always respect those UI & UX points: - .ts files must be formatted with "Prettier ESLint" - .svelte files must be formatted with "Svelte for VS Code" +- All CSS code **must** go into styles.css, and all classes should be properly named for easy customization. Do **not** use `