diff --git a/src/components/GlyphAddNote.svelte b/src/components/GlyphAddNote.svelte new file mode 100644 index 0000000..22832ad --- /dev/null +++ b/src/components/GlyphAddNote.svelte @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/components/ResultItemContainer.svelte b/src/components/ResultItemContainer.svelte index c65c61d..2eac865 100644 --- a/src/components/ResultItemContainer.svelte +++ b/src/components/ResultItemContainer.svelte @@ -1,8 +1,9 @@
+ {#if glyph} + + {/if}
diff --git a/src/components/ResultItemVault.svelte b/src/components/ResultItemVault.svelte index 03de740..8c33e81 100644 --- a/src/components/ResultItemVault.svelte +++ b/src/components/ResultItemVault.svelte @@ -1,4 +1,5 @@ - + {@html note.basename.replace(reg, highlighter)} diff --git a/src/globals.ts b/src/globals.ts index c21aa31..10b6cd5 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -28,6 +28,8 @@ export type IndexedNote = { headings1: string headings2: string headings3: string + + doesNotExist?: boolean } export type SearchMatch = { diff --git a/src/main.ts b/src/main.ts index 19b2860..e967b5c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,9 @@ import { Plugin, TFile } from 'obsidian' import { + addNonExistingToIndex, addToIndex, initGlobalSearchIndex, removeFromIndex, - removeFromIndexByPath, } from './search' import { OmnisearchInFileModal, OmnisearchVaultModal } from './modals' import { loadSettings, SettingsTab } from './settings' @@ -39,19 +39,21 @@ export default class OmnisearchPlugin extends Plugin { ) this.registerEvent( this.app.vault.on('delete', file => { - removeFromIndex(file) + removeFromIndex(file.path) + // Re-index the note as non-existing file + addNonExistingToIndex(file.name) }), ) this.registerEvent( this.app.vault.on('modify', async file => { - removeFromIndex(file) + removeFromIndex(file.path) await addToIndex(file) }), ) this.registerEvent( this.app.vault.on('rename', async (file, oldPath) => { if (file instanceof TFile && file.path.endsWith('.md')) { - removeFromIndexByPath(oldPath) + removeFromIndex(oldPath) await addToIndex(file) } }), diff --git a/src/notes.ts b/src/notes.ts index 2020b4f..4d23364 100644 --- a/src/notes.ts +++ b/src/notes.ts @@ -1,7 +1,27 @@ -import { MarkdownView } from 'obsidian' -import type { ResultNote } from './globals' +import { MarkdownView, TFile, type CachedMetadata } from 'obsidian' +import type { IndexedNote, ResultNote } from './globals' import { stringsToRegex } from './utils' +/** + * This is an in-memory cache of the notes, with all their computed fields + * used by the search engine. + * This cache allows us to quickly de-index notes when they are deleted or updated. + */ +export let notesCache: Record = {} + +export function resetNotesCache(): void { + notesCache = {} +} +export function getNoteFromCache(key: string): IndexedNote | undefined { + return notesCache[key] +} +export function addNoteToCache(key: string, note: IndexedNote): void { + notesCache[key] = note +} +export function removeNoteFromCache(key: string): void { + delete notesCache[key] +} + export async function openNote( item: ResultNote, newPane = false, @@ -40,3 +60,23 @@ export async function createNote(name: string): Promise { console.error(e) } } + +/** + * For a given file, returns a list of links leading to notes that don't exist + * @param file + * @param metadata + * @returns + */ +export function getNonExistingNotes( + file: TFile, + metadata: CachedMetadata, +): string[] { + return (metadata.links ?? []) + .map(l => { + const path = l.link.split(/[\^#]+/)[0] // Remove anchors and headings + return app.metadataCache.getFirstLinkpathDest(path, file.path) + ? '' + : l.link + }) + .filter(l => !!l) +} diff --git a/src/search.ts b/src/search.ts index 12e87ca..666761a 100644 --- a/src/search.ts +++ b/src/search.ts @@ -16,9 +16,15 @@ import { } from './utils' import type { Query } from './query' import { settings } from './settings' +import { + removeNoteFromCache, + getNoteFromCache, + getNonExistingNotes, + resetNotesCache, + addNoteToCache, +} from './notes' let minisearchInstance: MiniSearch -let indexedNotes: Record = {} const tokenize = (text: string): string[] => { const tokens = text.split(SPACE_OR_PUNCTUATION) @@ -37,7 +43,7 @@ const tokenize = (text: string): string[] => { * and adds all the notes to the index */ export async function initGlobalSearchIndex(): Promise { - indexedNotes = {} + resetNotesCache() minisearchInstance = new MiniSearch({ tokenize, idField: 'path', @@ -73,9 +79,6 @@ export async function initGlobalSearchIndex(): Promise { }ms`, ) } - - // Listen to the query input to trigger a search - // subscribeToQuery() } /** @@ -115,20 +118,20 @@ async function search(query: Query): Promise { const exactTerms = query.getExactTerms() if (exactTerms.length) { results = results.filter(r => { - const title = indexedNotes[r.id]?.path.toLowerCase() ?? '' + const title = getNoteFromCache(r.id)?.path.toLowerCase() ?? '' const content = stripMarkdownCharacters( - indexedNotes[r.id]?.content ?? '', + getNoteFromCache(r.id)?.content ?? '', ).toLowerCase() return exactTerms.every(q => content.includes(q) || title.includes(q)) }) } - // // If the search query contains exclude terms, filter out results that have them + // If the search query contains exclude terms, filter out results that have them const exclusions = query.exclusions if (exclusions.length) { results = results.filter(r => { const content = stripMarkdownCharacters( - indexedNotes[r.id]?.content ?? '', + getNoteFromCache(r.id)?.content ?? '', ).toLowerCase() return exclusions.every(q => !content.includes(q.value)) }) @@ -145,7 +148,9 @@ async function search(query: Query): Promise { export function getMatches(text: string, reg: RegExp): SearchMatch[] { let match: RegExpExecArray | null = null const matches: SearchMatch[] = [] + let count = 0 // TODO: FIXME: this is a hack to avoid infinite loops while ((match = reg.exec(text)) !== null) { + if (++count > 100) break const m = match[0] if (m) matches.push({ match: m, offset: match.index }) } @@ -181,7 +186,7 @@ export async function getSuggestions( // Map the raw results to get usable suggestions const suggestions = results.map(result => { - const note = indexedNotes[result.id] + const note = getNoteFromCache(result.id) if (!note) { throw new Error(`Note "${result.id}" not indexed`) } @@ -216,11 +221,27 @@ export async function addToIndex(file: TAbstractFile): Promise { if (!(file instanceof TFile) || file.extension !== 'md') { return } + + // Check if the file was already indexed as non-existent, + // and if so, remove it from the index (before adding it again) + if (getNoteFromCache(file.path)?.doesNotExist) { + removeFromIndex(file.path) + } + try { // console.log(`Omnisearch - adding ${file.path} to index`) - const metadata = app.metadataCache.getFileCache(file) - if (indexedNotes[file.path]) { + // Look for links that lead to non-existing files, + // and index them as well + const metadata = app.metadataCache.getFileCache(file) + if (metadata) { + const nonExisting = getNonExistingNotes(file, metadata) + for (const name of nonExisting.filter(o => !getNoteFromCache(o))) { + addNonExistingToIndex(name) + } + } + + if (getNoteFromCache(file.path)) { throw new Error(`${file.basename} is already indexed`) } @@ -245,7 +266,7 @@ export async function addToIndex(file: TAbstractFile): Promise { } minisearchInstance.add(note) - indexedNotes[note.path] = note + addNoteToCache(note.path, note) } catch (e) { console.trace('Error while indexing ' + file.basename) @@ -254,25 +275,42 @@ export async function addToIndex(file: TAbstractFile): Promise { } /** - * Removes a file from the index - * @param file - * @returns + * Index a non-existing note. + * Useful to find internal links that lead (yet) to nowhere + * @param name */ -export function removeFromIndex(file: TAbstractFile): void { - if (file instanceof TFile && file.path.endsWith('.md')) { - // console.log(`Omnisearch - removing ${file.path} from index`) - return removeFromIndexByPath(file.path) - } +export function addNonExistingToIndex(name: string): void { + const filename = name + (name.endsWith('.md') ? '' : '.md') + const note = { + path: filename, + basename: name, + content: '', + aliases: '', + headings1: '', + headings2: '', + headings3: '', + doesNotExist: true, + } as IndexedNote + minisearchInstance.add(note) + addNoteToCache(filename, note) } /** * Removes a file from the index, by its path * @param path */ -export function removeFromIndexByPath(path: string): void { - const note = indexedNotes[path] +export function removeFromIndex(path: string): void { + if (!path.endsWith('.md')) { + console.info(`"${path}" is not a .md file`) + return + } + const note = getNoteFromCache(path) if (note) { + // Delete the original minisearchInstance.remove(note) - delete indexedNotes[path] + removeNoteFromCache(path) + } + else { + console.warn(`not not found under path ${path}`) } } diff --git a/src/vendor/parse-query.ts b/src/vendor/parse-query.ts index 4543b88..281a13b 100644 --- a/src/vendor/parse-query.ts +++ b/src/vendor/parse-query.ts @@ -75,7 +75,9 @@ export function parseQuery( const regex = /(\S+:'(?:[^'\\]|\\.)*')|(\S+:"(?:[^"\\]|\\.)*")|(-?"(?:[^"\\]|\\.)*")|(-?'(?:[^'\\]|\\.)*')|\S+|\S+:\S+/g let match + let count = 0 // TODO: FIXME: this is a hack to avoid infinite loops while ((match = regex.exec(string)) !== null) { + if (++count > 100) break let term = match[0] const sepIndex = term.indexOf(':')