Merge branch 'develop'

# Conflicts:
#	manifest-beta.json
#	versions.json
This commit is contained in:
Simon Cambier
2024-10-26 11:04:33 +02:00
26 changed files with 1663 additions and 1026 deletions

View File

@@ -44,7 +44,7 @@ jobs:
id: git-cliff id: git-cliff
with: with:
config: cliff.toml config: cliff.toml
args: --verbose args: -vv --latest --strip header
env: env:
GITHUB_REPO: ${{ github.repository }} GITHUB_REPO: ${{ github.repository }}

View File

@@ -9,9 +9,7 @@ Please read this document before beginning work on a Pull Request.
## Preface ## 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. - 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. - 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
## "Good First Issue" ## "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 ## 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. - 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. - Use Svelte for all UI needs.
- Try to not shoehorn your code into existing functions or components. - 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" - .ts files must be formatted with "Prettier ESLint"
- .svelte files must be formatted with "Svelte for VS Code" - .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 `<style>` tags in Svelte components

View File

@@ -5,8 +5,8 @@
- Omnisearch is available on [the official Community Plugins repository](https://obsidian.md/plugins?search=Omnisearch). - 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.** - 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, ... > [!INFO] Chinese users
> If you have notes in a CJK language, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) > If you have notes in Chinese, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) for better search results.
## Documentation ## Documentation
@@ -22,8 +22,9 @@ Omnisearch is licensed under [GPL-3](https://tldrlegal.com/license/gnu-general-p
## Thanks ## 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)" :) 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)

View File

@@ -5,7 +5,7 @@
white-space: normal; white-space: normal;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; /* justify-content: space-between; */
flex-wrap: nowrap; flex-wrap: nowrap;
} }
@@ -59,6 +59,11 @@
margin-inline-start: 0.5em; margin-inline-start: 0.5em;
} }
.omnisearch-result__embed {
margin-left: 1em;
}
.omnisearch-result__image-container { .omnisearch-result__image-container {
flex-basis: 20%; flex-basis: 20%;
text-align: end; text-align: end;
@@ -82,6 +87,25 @@
gap: 5px; gap: 5px;
} }
.omnisearch-result__icon {
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
margin-right: 4px;
}
.omnisearch-result__icon svg {
width: 100%;
height: 100%;
}
.omnisearch-result__icon--emoji {
font-size: 16px;
vertical-align: middle;
margin-right: 4px;
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.omnisearch-input-container { .omnisearch-input-container {
flex-direction: column; flex-direction: column;

View File

@@ -1,7 +1,7 @@
{ {
"id": "omnisearch", "id": "omnisearch",
"name": "Omnisearch", "name": "Omnisearch",
"version": "1.25.0-beta.4", "version": "1.25.0-beta.2",
"minAppVersion": "1.3.0", "minAppVersion": "1.3.0",
"description": "A search engine that just works", "description": "A search engine that just works",
"author": "Simon Cambier", "author": "Simon Cambier",

View File

@@ -2,7 +2,7 @@
"id": "omnisearch", "id": "omnisearch",
"name": "Omnisearch", "name": "Omnisearch",
"version": "1.24.1", "version": "1.24.1",
"minAppVersion": "1.3.0", "minAppVersion": "1.7.2",
"description": "A search engine that just works", "description": "A search engine that just works",
"author": "Simon Cambier", "author": "Simon Cambier",
"authorUrl": "https://github.com/scambier/obsidian-omnisearch", "authorUrl": "https://github.com/scambier/obsidian-omnisearch",

View File

@@ -1,6 +1,6 @@
{ {
"name": "scambier.obsidian-search", "name": "scambier.obsidian-search",
"version": "1.24.1", "version": "1.25.0-beta.4",
"description": "A search engine for Obsidian", "description": "A search engine for Obsidian",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
@@ -14,13 +14,13 @@
"author": "Simon Cambier", "author": "Simon Cambier",
"license": "GPL-3", "license": "GPL-3",
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.8",
"@babel/preset-typescript": "^7.24.7", "@babel/preset-typescript": "^7.25.7",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^16.18.108", "@types/node": "^16.18.113",
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"babel-jest": "^27.5.1", "babel-jest": "^27.5.1",
"builtin-modules": "^3.3.0", "builtin-modules": "^3.3.0",
@@ -28,7 +28,7 @@
"esbuild-plugin-copy": "1.3.0", "esbuild-plugin-copy": "1.3.0",
"esbuild-svelte": "0.7.1", "esbuild-svelte": "0.7.1",
"jest": "^27.5.1", "jest": "^27.5.1",
"obsidian": "1.5.7-1", "obsidian": "1.7.2",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^3.59.2", "svelte": "^3.59.2",
@@ -37,14 +37,14 @@
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^3.2.10" "vite": "^3.2.11"
}, },
"dependencies": { "dependencies": {
"cancelable-promise": "^4.3.1", "cancelable-promise": "^4.3.1",
"dexie": "^3.2.7", "dexie": "^3.2.7",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"markdown-link-extractor": "^4.0.2", "markdown-link-extractor": "^4.0.2",
"minisearch": "github:scambier/minisearch#async-load-json", "minisearch": "7.1.0",
"pure-md5": "^0.1.14", "pure-md5": "^0.1.14",
"search-query-parser": "^1.6.0" "search-query-parser": "^1.6.0"
}, },

1843
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@
const debouncedOnInput = debounce(() => { const debouncedOnInput = debounce(() => {
// If typing a query and not executing it, // If typing a query and not executing it,
// the next time we open the modal, the search field will be empty // the next time we open the modal, the search field will be empty
plugin.cacheManager.addToSearchHistory('') plugin.searchHistory.addToHistory('')
dispatch('input', value) dispatch('input', value)
}, 300) }, 300)
</script> </script>

View File

@@ -110,7 +110,7 @@
async function prevSearchHistory() { async function prevSearchHistory() {
// Filter out the empty string, if it's there // Filter out the empty string, if it's there
const history = (await plugin.cacheManager.getSearchHistory()).filter( const history = (await plugin.searchHistory.getHistory()).filter(
s => s s => s
) )
if (++historySearchIndex >= history.length) { if (++historySearchIndex >= history.length) {
@@ -121,7 +121,7 @@
} }
async function nextSearchHistory() { async function nextSearchHistory() {
const history = (await plugin.cacheManager.getSearchHistory()).filter( const history = (await plugin.searchHistory.getHistory()).filter(
s => s s => s
) )
if (--historySearchIndex < 0) { if (--historySearchIndex < 0) {
@@ -192,7 +192,7 @@
function saveCurrentQuery() { function saveCurrentQuery() {
if (searchQuery) { if (searchQuery) {
plugin.cacheManager.addToSearchHistory(searchQuery) plugin.searchHistory.addToHistory(searchQuery)
} }
} }

View File

@@ -4,11 +4,12 @@
export let id: string export let id: string
export let selected = false export let selected = false
export let glyph = false export let glyph = false
export let cssClass = ''
</script> </script>
<div <div
data-result-id={id} data-result-id={id}
class="suggestion-item omnisearch-result" class="suggestion-item omnisearch-result {cssClass}"
class:is-selected={selected} class:is-selected={selected}
on:mousemove on:mousemove
on:click on:click

View File

@@ -3,14 +3,25 @@
import type { ResultNote } from '../globals' import type { ResultNote } from '../globals'
import { import {
getExtension, getExtension,
isFileCanvas, isFileExcalidraw, isFileCanvas,
isFileExcalidraw,
isFileImage, isFileImage,
isFilePDF, isFilePDF,
pathWithoutFilename, pathWithoutFilename,
} from '../tools/utils' } from '../tools/utils'
import ResultItemContainer from './ResultItemContainer.svelte' import ResultItemContainer from './ResultItemContainer.svelte'
import { TFile, setIcon } from 'obsidian'
import type OmnisearchPlugin from '../main' import type OmnisearchPlugin from '../main'
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 selected = false
export let note: ResultNote export let note: ResultNote
@@ -19,8 +30,77 @@
let imagePath: string | null = null let imagePath: string | null = null
let title = '' let title = ''
let notePath = '' 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 elFolderPathIcon: HTMLElement
let elFilePathIcon: HTMLElement let elFilePathIcon: HTMLElement
let elEmbedIcon: HTMLElement
$: { $: {
imagePath = null imagePath = null
@@ -31,9 +111,16 @@
} }
} }
} }
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords) $: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
$: matchesNotePath = plugin.textProcessor.getMatches(notePath, note.foundWords) $: matchesNotePath = plugin.textProcessor.getMatches(
$: cleanedContent = plugin.textProcessor.makeExcerpt(note.content, note.matches[0]?.offset ?? -1) notePath,
note.foundWords
)
$: cleanedContent = plugin.textProcessor.makeExcerpt(
note.content,
note.matches[0]?.offset ?? -1
)
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist $: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
$: { $: {
title = note.displayTitle || note.basename title = note.displayTitle || note.basename
@@ -46,23 +133,24 @@
if (elFilePathIcon) { if (elFilePathIcon) {
if (isFileImage(note.path)) { if (isFileImage(note.path)) {
setIcon(elFilePathIcon, 'image') setIcon(elFilePathIcon, 'image')
} } else if (isFilePDF(note.path)) {
else if (isFilePDF(note.path)) {
setIcon(elFilePathIcon, 'file-text') setIcon(elFilePathIcon, 'file-text')
} } else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
setIcon(elFilePathIcon, 'layout-dashboard') setIcon(elFilePathIcon, 'layout-dashboard')
} } else {
else {
setIcon(elFilePathIcon, 'file') setIcon(elFilePathIcon, 'file')
} }
} }
if (elEmbedIcon) {
setIcon(elEmbedIcon, 'corner-down-right')
}
} }
</script> </script>
<ResultItemContainer <ResultItemContainer
glyph="{glyph}" glyph="{glyph}"
id="{note.path}" id="{note.path}"
cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}"
on:auxclick on:auxclick
on:click on:click
on:mousemove on:mousemove
@@ -70,8 +158,19 @@
<div> <div>
<div class="omnisearch-result__title-container"> <div class="omnisearch-result__title-container">
<span class="omnisearch-result__title"> <span class="omnisearch-result__title">
<span bind:this="{elFilePathIcon}"></span> {#if note.isEmbed}
<span>{@html plugin.textProcessor.highlightText(title, matchesTitle)}</span> <span
bind:this="{elEmbedIcon}"
title="The document above is embedded in this note"></span>
{:else}
<!-- File Icon -->
{#if fileIconSVG}
<span class="omnisearch-result__icon" use:renderSVG="{fileIconSVG}"></span>
{/if}
{/if}
<span>
{@html plugin.textProcessor.highlightText(title, matchesTitle)}
</span>
<span class="omnisearch-result__extension"> <span class="omnisearch-result__extension">
.{getExtension(note.path)} .{getExtension(note.path)}
</span> </span>
@@ -90,24 +189,35 @@
<!-- Folder path --> <!-- Folder path -->
{#if notePath} {#if notePath}
<div class="omnisearch-result__folder-path"> <div class="omnisearch-result__folder-path">
<span bind:this="{elFolderPathIcon}"></span> <!-- Folder Icon -->
<span>{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}</span> {#if folderIconSVG}
<span class="omnisearch-result__icon" use:renderSVG="{folderIconSVG}"></span>
{/if}
<span>
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
</span>
</div> </div>
{/if} {/if}
<div style="display: flex; flex-direction: row;"> <!-- Do not display the excerpt for embedding references -->
{#if $showExcerpt} {#if !note.isEmbed}
<div class="omnisearch-result__body"> <div style="display: flex; flex-direction: row;">
{@html plugin.textProcessor.highlightText(cleanedContent, note.matches)} {#if $showExcerpt}
</div> <div class="omnisearch-result__body">
{/if} {@html plugin.textProcessor.highlightText(
cleanedContent,
note.matches
)}
</div>
{/if}
<!-- Image --> <!-- Image -->
{#if imagePath} {#if imagePath}
<div class="omnisearch-result__image-container"> <div class="omnisearch-result__image-container">
<img style="width: 100px" src="{imagePath}" alt="" /> <img style="width: 100px" src="{imagePath}" alt="" />
</div> </div>
{/if} {/if}
</div> </div>
{/if}
</div> </div>
</ResultItemContainer> </ResultItemContainer>

View File

@@ -163,7 +163,7 @@ export class OmnisearchVaultModal extends OmnisearchModal {
.getActiveViewOfType(MarkdownView) .getActiveViewOfType(MarkdownView)
?.editor.getSelection() ?.editor.getSelection()
plugin.cacheManager.getSearchHistory().then(history => { plugin.searchHistory.getHistory().then(history => {
// Previously searched query (if enabled in settings) // Previously searched query (if enabled in settings)
const previous = plugin.settings.showPreviousQueryResults const previous = plugin.settings.showPreviousQueryResults
? history[0] ? history[0]

View File

@@ -1,12 +1,11 @@
import Dexie from 'dexie' import Dexie from 'dexie'
import type MiniSearch from 'minisearch'
import type { AsPlainObject } from 'minisearch' import type { AsPlainObject } from 'minisearch'
import type { DocumentRef } from './globals' import type { DocumentRef } from './globals'
import { Notice } from 'obsidian' import { Notice } from 'obsidian'
import type OmnisearchPlugin from './main' import type OmnisearchPlugin from './main'
export class Database extends Dexie { export class Database extends Dexie {
public static readonly dbVersion = 8 public static readonly dbVersion = 10
searchHistory!: Dexie.Table<{ id?: number; query: string }, number> searchHistory!: Dexie.Table<{ id?: number; query: string }, number>
minisearch!: Dexie.Table< minisearch!: Dexie.Table<
{ {
@@ -16,6 +15,7 @@ export class Database extends Dexie {
}, },
string string
> >
embeds!: Dexie.Table<{ embedded: string; referencedBy: string[] }, string>
constructor(private plugin: OmnisearchPlugin) { constructor(private plugin: OmnisearchPlugin) {
super(Database.getDbName(plugin.app.appId)) super(Database.getDbName(plugin.app.appId))
@@ -23,6 +23,7 @@ export class Database extends Dexie {
this.version(Database.dbVersion).stores({ this.version(Database.dbVersion).stores({
searchHistory: '++id', searchHistory: '++id',
minisearch: 'date', minisearch: 'date',
embeds: 'embedded',
}) })
} }
@@ -49,17 +50,15 @@ export class Database extends Dexie {
} }
} }
public async writeMinisearchCache( public async writeMinisearchCache(): Promise<void> {
minisearch: MiniSearch, const minisearchJson = this.plugin.searchEngine.getSerializedMiniSearch()
indexed: Map<string, number> const paths = this.plugin.searchEngine.getSerializedIndexedDocuments()
): Promise<void> {
const paths = Array.from(indexed).map(([k, v]) => ({ path: k, mtime: v }))
const database = this.plugin.database const database = this.plugin.database
await database.minisearch.clear() await database.minisearch.clear()
await database.minisearch.add({ await database.minisearch.add({
date: new Date().toISOString(), date: new Date().toISOString(),
paths, paths,
data: minisearch.toJSON(), data: minisearchJson,
}) })
console.log('Omnisearch - Search cache written') console.log('Omnisearch - Search cache written')
} }
@@ -85,7 +84,8 @@ export class Database extends Dexie {
} }
public async clearCache() { public async clearCache() {
new Notice('Omnisearch - Cache cleared. Please restart Obsidian.')
await this.minisearch.clear() await this.minisearch.clear()
await this.embeds.clear()
new Notice('Omnisearch - Cache cleared. Please restart Obsidian.')
} }
} }

View File

@@ -81,6 +81,7 @@ export type ResultNote = {
content: string content: string
foundWords: string[] foundWords: string[]
matches: SearchMatch[] matches: SearchMatch[]
isEmbed: boolean
} }
let inComposition = false let inComposition = false

View File

@@ -24,30 +24,33 @@ import {
import { notifyOnIndexed, registerAPI } from './tools/api' import { notifyOnIndexed, registerAPI } from './tools/api'
import { Database } from './database' import { Database } from './database'
import { SearchEngine } from './search/search-engine' import { SearchEngine } from './search/search-engine'
import { CacheManager } from './cache-manager' import { DocumentsRepository } from './repositories/documents-repository'
import { logDebug } from './tools/utils' import { logDebug } from './tools/utils'
import { NotesIndexer } from './notes-indexer' import { NotesIndexer } from './notes-indexer'
import { TextProcessor } from './tools/text-processing' import { TextProcessor } from './tools/text-processing'
import { EmbedsRepository } from './repositories/embeds-repository'
import { SearchHistory } from "./search/search-history";
export default class OmnisearchPlugin extends Plugin { export default class OmnisearchPlugin extends Plugin {
// FIXME: fix the type // FIXME: fix the type
public apiHttpServer: null | any = null public apiHttpServer: null | any = null
public settings: OmnisearchSettings = getDefaultSettings(this.app) public settings: OmnisearchSettings = getDefaultSettings(this.app)
// FIXME: merge cache and cacheManager, or find other names public readonly documentsRepository: DocumentsRepository
public readonly cacheManager: CacheManager public readonly embedsRepository = new EmbedsRepository(this)
public readonly database = new Database(this) public readonly database = new Database(this)
public readonly notesIndexer = new NotesIndexer(this) public readonly notesIndexer = new NotesIndexer(this)
public readonly textProcessor = new TextProcessor(this) public readonly textProcessor = new TextProcessor(this)
public readonly searchEngine = new SearchEngine(this) public readonly searchEngine = new SearchEngine(this)
public readonly searchHistory = new SearchHistory(this)
private ribbonButton?: HTMLElement private ribbonButton?: HTMLElement
private refreshIndexCallback?: () => void private refreshIndexCallback?: () => void
constructor(app: App, manifest: PluginManifest) { constructor(app: App, manifest: PluginManifest) {
super(app, manifest) super(app, manifest)
this.cacheManager = new CacheManager(this) this.documentsRepository = new DocumentsRepository(this)
} }
async onload(): Promise<void> { async onload(): Promise<void> {
@@ -109,14 +112,16 @@ export default class OmnisearchPlugin extends Plugin {
if (this.notesIndexer.isFileIndexable(file.path)) { if (this.notesIndexer.isFileIndexable(file.path)) {
logDebug('Indexing new file', file.path) logDebug('Indexing new file', file.path)
searchEngine.addFromPaths([file.path]) searchEngine.addFromPaths([file.path])
this.embedsRepository.refreshEmbedsForNote(file.path)
} }
}) })
) )
this.registerEvent( this.registerEvent(
this.app.vault.on('delete', file => { this.app.vault.on('delete', file => {
logDebug('Removing file', file.path) logDebug('Removing file', file.path)
this.cacheManager.removeFromLiveCache(file.path) this.documentsRepository.removeDocument(file.path)
searchEngine.removeFromPaths([file.path]) searchEngine.removeFromPaths([file.path])
this.embedsRepository.removeFile(file.path)
}) })
) )
this.registerEvent( this.registerEvent(
@@ -124,16 +129,20 @@ export default class OmnisearchPlugin extends Plugin {
if (this.notesIndexer.isFileIndexable(file.path)) { if (this.notesIndexer.isFileIndexable(file.path)) {
this.notesIndexer.flagNoteForReindex(file) this.notesIndexer.flagNoteForReindex(file)
} }
this.embedsRepository.refreshEmbedsForNote(file.path)
}) })
) )
this.registerEvent( this.registerEvent(
this.app.vault.on('rename', async (file, oldPath) => { this.app.vault.on('rename', async (file, oldPath) => {
if (this.notesIndexer.isFileIndexable(file.path)) { if (this.notesIndexer.isFileIndexable(file.path)) {
logDebug('Renaming file', file.path) logDebug('Renaming file', file.path)
this.cacheManager.removeFromLiveCache(oldPath) this.documentsRepository.removeDocument(oldPath)
await this.cacheManager.addToLiveCache(file.path) await this.documentsRepository.addDocument(file.path)
searchEngine.removeFromPaths([oldPath]) searchEngine.removeFromPaths([oldPath])
await searchEngine.addFromPaths([file.path]) await searchEngine.addFromPaths([file.path])
this.embedsRepository.renameFile(oldPath, file.path)
} }
}) })
) )
@@ -240,7 +249,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 })) files.map(f => ({ path: f.path, mtime: f.stat.mtime }))
) )
@@ -281,7 +290,8 @@ export default class OmnisearchPlugin extends Plugin {
} }
// Write the cache // Write the cache
await searchEngine.writeToCache() await this.database.writeMinisearchCache()
await this.embedsRepository.writeToCache()
// Re-enable settings.caching // Re-enable settings.caching
if (cacheEnabled) { if (cacheEnabled) {

View File

@@ -26,7 +26,7 @@ export class NotesIndexer {
public async refreshIndex(): Promise<void> { public async refreshIndex(): Promise<void> {
for (const file of this.notesToReindex) { for (const file of this.notesToReindex) {
logDebug('Updating file', file.path) logDebug('Updating file', file.path)
await this.plugin.cacheManager.addToLiveCache(file.path) await this.plugin.documentsRepository.addDocument(file.path)
} }
const paths = [...this.notesToReindex].map(n => n.path) const paths = [...this.notesToReindex].map(n => n.path)

View File

@@ -1,5 +1,5 @@
import { TFile } from 'obsidian' import { normalizePath, Notice, TFile } from 'obsidian'
import type { IndexedDocument } from './globals' import type { IndexedDocument } from '../globals'
import { import {
extractHeadingsFromCache, extractHeadingsFromCache,
getAliasesFromMetadata, getAliasesFromMetadata,
@@ -12,30 +12,33 @@ import {
logDebug, logDebug,
removeDiacritics, removeDiacritics,
stripMarkdownCharacters, stripMarkdownCharacters,
} from './tools/utils' } from '../tools/utils'
import type { CanvasData } from 'obsidian/canvas' import type { CanvasData } from 'obsidian/canvas'
import type OmnisearchPlugin from './main' import type OmnisearchPlugin from '../main'
import { getNonExistingNotes } from './tools/notes' import { getNonExistingNotes } from '../tools/notes'
export class CacheManager {
/**
* Show an empty input field next time the user opens Omnisearch modal
*/
private nextQueryIsEmpty = false
export class DocumentsRepository {
/** /**
* The "live cache", containing all indexed vault files * The "live cache", containing all indexed vault files
* in the form of IndexedDocuments * in the form of IndexedDocuments
*/ */
private documents: Map<string, IndexedDocument> = new Map() private documents: Map<string, IndexedDocument> = new Map()
private errorsCount = 0
private errorsWarned = false
constructor(private plugin: OmnisearchPlugin) {} constructor(private plugin: OmnisearchPlugin) {
setInterval(() => {
if (this.errorsCount > 0) {
--this.errorsCount
}
}, 1000)
}
/** /**
* Set or update the live cache with the content of the given file. * Set or update the live cache with the content of the given file.
* @param path * @param path
*/ */
public async addToLiveCache(path: string): Promise<void> { public async addDocument(path: string): Promise<void> {
try { try {
const doc = await this.getAndMapIndexedDocument(path) const doc = await this.getAndMapIndexedDocument(path)
if (!doc.path) { if (!doc.path) {
@@ -45,14 +48,16 @@ export class CacheManager {
return return
} }
this.documents.set(path, doc) this.documents.set(path, doc)
this.plugin.embedsRepository.refreshEmbedsForNote(path)
} catch (e) { } catch (e) {
console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e) console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e)
// Shouldn't be needed, but... // Shouldn't be needed, but...
this.removeFromLiveCache(path) this.removeDocument(path)
this.countError()
} }
} }
public removeFromLiveCache(path: string): void { public removeDocument(path: string): void {
this.documents.delete(path) this.documents.delete(path)
} }
@@ -61,38 +66,10 @@ export class CacheManager {
return this.documents.get(path)! return this.documents.get(path)!
} }
logDebug('Generating IndexedDocument from', path) logDebug('Generating IndexedDocument from', path)
await this.addToLiveCache(path) await this.addDocument(path)
return this.documents.get(path)! return this.documents.get(path)!
} }
public async addToSearchHistory(query: string): Promise<void> {
if (!query) {
this.nextQueryIsEmpty = true
return
}
this.nextQueryIsEmpty = false
const database = this.plugin.database
let history = await database.searchHistory.toArray()
history = history.filter(s => s.query !== query).reverse()
history.unshift({ query })
history = history.slice(0, 10)
await database.searchHistory.clear()
await database.searchHistory.bulkAdd(history)
}
/**
* @returns The search history, in reverse chronological order
*/
public async getSearchHistory(): Promise<ReadonlyArray<string>> {
const data = (await this.plugin.database.searchHistory.toArray())
.reverse()
.map(o => o.query)
if (this.nextQueryIsEmpty) {
data.unshift('')
}
return data
}
/** /**
* This function is responsible for extracting the text from a file and * This function is responsible for extracting the text from a file and
* returning it as an `IndexedDocument` object. * returning it as an `IndexedDocument` object.
@@ -101,6 +78,7 @@ export class CacheManager {
private async getAndMapIndexedDocument( private async getAndMapIndexedDocument(
path: string path: string
): Promise<IndexedDocument> { ): Promise<IndexedDocument> {
path = normalizePath(path)
const app = this.plugin.app const app = this.plugin.app
const file = app.vault.getAbstractFileByPath(path) const file = app.vault.getAbstractFileByPath(path)
if (!file) throw new Error(`Invalid file path: "${path}"`) if (!file) throw new Error(`Invalid file path: "${path}"`)
@@ -163,16 +141,22 @@ export class CacheManager {
else if ( else if (
isFileImage(path) && isFileImage(path) &&
((this.plugin.settings.imagesIndexing && ((this.plugin.settings.imagesIndexing &&
extractor?.canFileBeExtracted(path)) || extractor?.canFileBeExtracted(path)) ||
(this.plugin.settings.aiImageIndexing && (this.plugin.settings.aiImageIndexing &&
aiImageAnalyzer?.canBeAnalyzed(file))) aiImageAnalyzer?.canBeAnalyzed(file)))
) { ) {
if (this.plugin.settings.imagesIndexing && extractor?.canFileBeExtracted(path)){ if (
this.plugin.settings.imagesIndexing &&
extractor?.canFileBeExtracted(path)
) {
content = await extractor.extractText(file) content = await extractor.extractText(file)
} }
if (this.plugin.settings.aiImageIndexing && aiImageAnalyzer?.canBeAnalyzed(file)) { if (
content = await aiImageAnalyzer.analyzeImage(file) + (content ?? '') this.plugin.settings.aiImageIndexing &&
aiImageAnalyzer?.canBeAnalyzed(file)
) {
content = (await aiImageAnalyzer.analyzeImage(file)) + (content ?? '')
} }
} }
// ** PDF ** // ** PDF **
@@ -230,7 +214,8 @@ export class CacheManager {
} }
} }
} }
const displayTitle = metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? '' const displayTitle =
metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? ''
const tags = getTagsFromMetadata(metadata) const tags = getTagsFromMetadata(metadata)
return { return {
basename: file.basename, basename: file.basename,
@@ -255,4 +240,13 @@ export class CacheManager {
: '', : '',
} }
} }
private countError(): void {
if (++this.errorsCount > 5 && !this.errorsWarned) {
this.errorsWarned = true
new Notice(
'Omnisearch ⚠️ There might be an issue with your cache. You should clean it in Omnisearch settings and restart Obsidian.'
)
}
}
} }

View File

@@ -0,0 +1,106 @@
import { getLinkpath, Notice } from 'obsidian'
import type OmnisearchPlugin from '../main'
import { logDebug } from '../tools/utils'
export class EmbedsRepository {
/** Map<embedded file, notes where the embed is referenced> */
private embeds: Map<string, Set<string>> = 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 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)
}
})
}
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[] {
const embeds = this.embeds.has(pathEmbedded)
? [...this.embeds.get(pathEmbedded)!]
: []
return embeds
}
public async writeToCache(): Promise<void> {
logDebug('Writing embeds to cache')
const database = this.plugin.database
const data: { embedded: string; referencedBy: string[] }[] = []
for (const [path, embedsList] of this.embeds) {
data.push({ embedded: path, referencedBy: [...embedsList] })
}
await database.embeds.clear()
await database.embeds.bulkAdd(data)
}
public async loadFromCache(): Promise<void> {
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 addEmbedsForNote(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)
}
}
}

View File

@@ -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 type { DocumentRef, IndexedDocument, ResultNote } from '../globals'
import { chunkArray, logDebug, removeDiacritics } from '../tools/utils' import { chunkArray, logDebug, removeDiacritics } from '../tools/utils'
@@ -13,6 +17,7 @@ export class SearchEngine {
private minisearch: MiniSearch private minisearch: MiniSearch
/** Map<path, mtime> */ /** Map<path, mtime> */
private indexedDocuments: Map<string, number> = new Map() private indexedDocuments: Map<string, number> = new Map()
// private previousResults: SearchResult[] = [] // private previousResults: SearchResult[] = []
// private previousQuery: Query | null = null // private previousQuery: Query | null = null
@@ -25,6 +30,7 @@ export class SearchEngine {
* Return true if the cache is valid * Return true if the cache is valid
*/ */
async loadCache(): Promise<boolean> { async loadCache(): Promise<boolean> {
await this.plugin.embedsRepository.loadFromCache()
const cache = await this.plugin.database.getMinisearchCache() const cache = await this.plugin.database.getMinisearchCache()
if (cache) { if (cache) {
this.minisearch = await MiniSearch.loadJSAsync( 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 * @param docs
*/ */
getDiff(docs: DocumentRef[]): { getDocumentsToReindex(docs: DocumentRef[]): {
toAdd: DocumentRef[] toAdd: DocumentRef[]
toRemove: DocumentRef[] toRemove: DocumentRef[]
} { } {
@@ -72,7 +79,7 @@ export class SearchEngine {
let documents = ( let documents = (
await Promise.all( await Promise.all(
paths.map( paths.map(
async path => await this.plugin.cacheManager.getDocument(path) async path => await this.plugin.documentsRepository.getDocument(path)
) )
) )
).filter(d => !!d?.path) ).filter(d => !!d?.path)
@@ -167,7 +174,7 @@ export class SearchEngine {
tokenize: text => [text], 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) // Filter query results to only keep files that match query.query.ext (if any)
if (query.query.ext?.length) { if (query.query.ext?.length) {
@@ -264,9 +271,9 @@ export class SearchEngine {
} }
} }
// Boost custom properties
const metadata = this.plugin.app.metadataCache.getCache(path) const metadata = this.plugin.app.metadataCache.getCache(path)
if (metadata) { if (metadata) {
// Boost custom properties
for (const { name, weight } of settings.weightCustomProperties) { for (const { name, weight } of settings.weightCustomProperties) {
const values = metadata?.frontmatter?.[name] const values = metadata?.frontmatter?.[name]
if (values && result.terms.some(t => values.includes(t))) { if (values && result.terms.some(t => values.includes(t))) {
@@ -288,11 +295,13 @@ export class SearchEngine {
// Sort results and keep the 50 best // Sort results and keep the 50 best
results = results.sort((a, b) => b.score - a.score).slice(0, 50) results = results.sort((a, b) => b.score - a.score).slice(0, 50)
logDebug('Filtered results:', results)
if (results.length) logDebug('First result:', results[0]) if (results.length) logDebug('First result:', results[0])
const documents = await Promise.all( const documents = await Promise.all(
results.map( results.map(
async result => await this.plugin.cacheManager.getDocument(result.id) async result => await this.plugin.documentsRepository.getDocument(result.id)
) )
) )
@@ -368,10 +377,37 @@ export class SearchEngine {
const documents = await Promise.all( const documents = await Promise.all(
results.map( results.map(
async result => await this.plugin.cacheManager.getDocument(result.id) async result => await this.plugin.documentsRepository.getDocument(result.id)
) )
) )
// Inject embeds for images, documents, and PDFs
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)
.slice(0, this.plugin.settings.maxEmbeds)
// Inject embeds in the results
for (const embed of embeds) {
total++
const newDoc = await this.plugin.documentsRepository.getDocument(embed)
documents.splice(i + 1, 0, newDoc)
results.splice(i + 1, 0, {
id: newDoc.path,
score: 0,
terms: [],
queryTerms: [],
match: {},
isEmbed: true,
})
i++ // Increment i to skip the newly inserted document
}
}
// Map the raw results to get usable suggestions // Map the raw results to get usable suggestions
const resultNotes = results.map(result => { const resultNotes = results.map(result => {
logDebug('Locating matches for', result.id) logDebug('Locating matches for', result.id)
@@ -407,23 +443,37 @@ export class SearchEngine {
foundWords, foundWords,
query query
) )
logDebug(`Matches for ${note.basename}`, matches) logDebug(`Matches for note "${note.path}"`, matches)
const resultNote: ResultNote = { const resultNote: ResultNote = {
score: result.score, score: result.score,
foundWords, foundWords,
matches, matches,
isEmbed: result.isEmbed,
...note, ...note,
} }
return resultNote return resultNote
}) })
logDebug('Suggestions:', resultNotes)
return resultNotes return resultNotes
} }
public async writeToCache(): Promise<void> { /**
await this.plugin.database.writeMinisearchCache( * For cache saving
this.minisearch, */
this.indexedDocuments 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<IndexedDocument> { private getOptions(): Options<IndexedDocument> {

View File

@@ -0,0 +1,38 @@
import type OmnisearchPlugin from '../main'
export class SearchHistory {
/**
* Show an empty input field next time the user opens Omnisearch modal
*/
private nextQueryIsEmpty = false
constructor(private plugin: OmnisearchPlugin) {}
public async addToHistory(query: string): Promise<void> {
if (!query) {
this.nextQueryIsEmpty = true
return
}
this.nextQueryIsEmpty = false
const database = this.plugin.database
let history = await database.searchHistory.toArray()
history = history.filter(s => s.query !== query).reverse()
history.unshift({ query })
history = history.slice(0, 10)
await database.searchHistory.clear()
await database.searchHistory.bulkAdd(history)
}
/**
* @returns The search history, in reverse chronological order
*/
public async getHistory(): Promise<ReadonlyArray<string>> {
const data = (await this.plugin.database.searchHistory.toArray())
.reverse()
.map(o => o.query)
if (this.nextQueryIsEmpty) {
data.unshift('')
}
return data
}
}

View File

@@ -54,6 +54,8 @@ export interface OmnisearchSettings extends WeightingSettings {
ribbonIcon: boolean ribbonIcon: boolean
/** Display the small contextual excerpt in search results */ /** Display the small contextual excerpt in search results */
showExcerpt: boolean showExcerpt: boolean
/** Number of embeds references to display in search results */
maxEmbeds: number
/** Render line returns with <br> in excerpts */ /** Render line returns with <br> in excerpts */
renderLineReturnInExcerpts: boolean renderLineReturnInExcerpts: boolean
/** Enable a "create note" button in the Vault Search modal */ /** 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 <a href="https://help.obsidian.md/Linking+notes+and+files/Embed+files">embedded</a> in other notes.<br>
This setting controls the maximum number of embeds to show in the search results. Set to 0 to disable.<br>
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 // Keep line returns in excerpts
new Setting(containerEl) new Setting(containerEl)
.setName('Render line return in excerpts') .setName('Render line return in excerpts')
@@ -761,7 +781,7 @@ export class SettingsTab extends PluginSettingTab {
} }
weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void { weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void {
cb.setLimits(1, 5, 0.1) cb.setLimits(1, 10, 0.5)
.setValue(settings[key]) .setValue(settings[key])
.setDynamicTooltip() .setDynamicTooltip()
.onChange(async v => { .onChange(async v => {
@@ -791,6 +811,7 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
ribbonIcon: true, ribbonIcon: true,
showExcerpt: true, showExcerpt: true,
maxEmbeds: 5,
renderLineReturnInExcerpts: true, renderLineReturnInExcerpts: true,
showCreateButton: false, showCreateButton: false,
highlight: true, highlight: true,
@@ -799,12 +820,12 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
tokenizeUrls: false, tokenizeUrls: false,
fuzziness: '1', fuzziness: '1',
weightBasename: 3, weightBasename: 10,
weightDirectory: 2, weightDirectory: 7,
weightH1: 1.5, weightH1: 6,
weightH2: 1.3, weightH2: 5,
weightH3: 1.1, weightH3: 4,
weightUnmarkedTags: 1.1, weightUnmarkedTags: 2,
weightCustomProperties: [] as { name: string; weight: number }[], weightCustomProperties: [] as { name: string; weight: number }[],
httpApiEnabled: false, httpApiEnabled: false,

188
src/tools/icon-utils.ts Normal file
View File

@@ -0,0 +1,188 @@
import { TFile, getIcon, normalizePath } from 'obsidian'
import type OmnisearchPlugin from '../main'
import {
isFileImage,
isFilePDF,
isFileCanvas,
isFileExcalidraw,
warnDebug,
} from './utils'
import { escapeHTML } from './text-processing'
export interface IconPacks {
prefixToIconPack: { [prefix: string]: string }
iconsPath: string
}
export async function loadIconData(plugin: OmnisearchPlugin): Promise<any> {
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<IconPacks> {
// 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<string | null> {
const parsed = parseIconName(iconName)
const { prefix, name } = parsed
if (!prefix) {
// No prefix, assume it's an emoji or text
return `<span class="omnisearch-result__icon--emoji">${escapeHTML(name)}</span>`
}
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 : ''
}

View File

@@ -78,15 +78,6 @@ export class TextProcessor {
} }
} }
escapeHTML(html: string): string {
return html
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}
/** /**
* Converts a list of strings to a list of words, using the \b word boundary. * Converts a list of strings to a list of words, using the \b word boundary.
* Used to find excerpts in a note body, or select which words to highlight. * Used to find excerpts in a note body, or select which words to highlight.
@@ -200,7 +191,7 @@ export class TextProcessor {
} }
} }
function escapeHTML(html: string): string { export function escapeHTML(html: string): string {
return html return html
.replaceAll('&', '&amp;') .replaceAll('&', '&amp;')
.replaceAll('<', '&lt;') .replaceAll('<', '&lt;')

View File

@@ -151,7 +151,7 @@ export function getCtrlKeyLabel(): 'ctrl' | '⌘' {
export function isFileImage(path: string): boolean { export function isFileImage(path: string): boolean {
const ext = getExtension(path) 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 { export function isFilePDF(path: string): boolean {

View File

@@ -149,7 +149,5 @@
"1.24.0": "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.1": "1.3.0",
"1.25.0-beta.2": "1.3.0", "1.25.0-beta.2": "1.3.0"
"1.25.0-beta.3": "1.3.0",
"1.25.0-beta.4": "1.3.0"
} }