From 98c277e541d9325905efde9ff8b5923cb443bd8d Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Thu, 14 Apr 2022 20:41:27 +0200 Subject: [PATCH] Updated the weighting logic + doing some preparation for #6 Headings now have more weight. Also set TS strict mode --- src/globals.ts | 15 +++++++- src/main.ts | 21 ++++++----- src/modal.ts | 97 ++++++++++++++++++++++++++++++-------------------- src/utils.ts | 31 +++++++++++++++- tsconfig.json | 1 + 5 files changed, 117 insertions(+), 48 deletions(-) diff --git a/src/globals.ts b/src/globals.ts index 6560d3c..7d8daa3 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,3 +1,5 @@ +import { match } from 'assert' + // Matches a wikiling that begins a string export const regexWikilink = /^!?\[\[(?.+?)(\|(?.+?))?\]\]/ export const regexLineSplit = /\r?\n|\r|((\.|\?|!)( |\r?\n|\r))/g @@ -13,12 +15,23 @@ export type IndexedNote = { path: string basename: string content: string + headings1: string + headings2: string + headings3: string +} + +export type SearchMatch = { + match: string + index: number +} +export const isSearchMatch = (o: { index?: number }): o is SearchMatch => { + return o.index !== undefined } export type ResultNote = { path: string basename: string content: string - keyword: string + matches: SearchMatch[] occurence: number } diff --git a/src/main.ts b/src/main.ts index 3f53cc0..9dabc41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,17 @@ import { Notice, Plugin, TAbstractFile, TFile } from 'obsidian' import MiniSearch from 'minisearch' -import { clearContent, getTitleLine, wait } from './utils' +import { + clearContent, + extractHeadingsFromCache, + wait, +} from './utils' import { IndexedNote } from './globals' import { OmnisearchModal } from './modal' export default class OmnisearchPlugin extends Plugin { - minisearch: MiniSearch + minisearch!: MiniSearch lastSearch?: string - indexedNotes: Record + indexedNotes: Record = {} async onload(): Promise { await this.instantiateMinisearch() @@ -53,11 +57,7 @@ export default class OmnisearchPlugin extends Plugin { this.indexedNotes = {} this.minisearch = new MiniSearch({ idField: 'path', - fields: ['content', 'title', 'basename'], - extractField: (document, fieldname: 'content' | 'title' | 'basename') => { - if (fieldname === 'title') return getTitleLine(document.content) - return document[fieldname] - }, + fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'], }) // Index files that are already present @@ -89,6 +89,8 @@ export default class OmnisearchPlugin extends Plugin { if (!(file instanceof TFile) || file.extension !== 'md') return try { // console.log(`Omnisearch - adding ${file.path} to index`) + const fileCache = this.app.metadataCache.getFileCache(file) + // console.log(fileCache) if (this.indexedNotes[file.path]) { throw new Error(`${file.basename} is already indexed`) @@ -106,6 +108,9 @@ export default class OmnisearchPlugin extends Plugin { basename: file.basename, content: tmp.innerText, path: file.path, + headings1: fileCache ? extractHeadingsFromCache(fileCache, 1).join(' ') : '', + headings2: fileCache ? extractHeadingsFromCache(fileCache, 2).join(' ') : '', + headings3: fileCache ? extractHeadingsFromCache(fileCache, 3).join(' ') : '', } this.minisearch.add(note) this.indexedNotes[file.path] = note diff --git a/src/modal.ts b/src/modal.ts index dd3726e..952f8a8 100644 --- a/src/modal.ts +++ b/src/modal.ts @@ -1,7 +1,7 @@ import { MarkdownView, SuggestModal, TFile } from 'obsidian' import { ResultNote } from './globals' import OmnisearchPlugin from './main' -import { escapeRegex, highlighter } from './utils' +import { escapeRegex, getAllIndexes, highlighter } from './utils' export class OmnisearchModal extends SuggestModal { private plugin: OmnisearchPlugin @@ -43,9 +43,13 @@ export class OmnisearchModal extends SuggestModal { await this.app.workspace.openLinkText(file.path, '') } catch (e) { - if (e.message === 'File already exists.') { + if (e instanceof Error && e.message === 'File already exists.') { + // Open the existing file instead of creating it await this.app.workspace.openLinkText(this.inputEl.value, '') } + else { + console.error(e) + } } } this.close() @@ -60,7 +64,7 @@ export class OmnisearchModal extends SuggestModal { const record = events.find(event => (event.target as HTMLDivElement).classList.contains('is-selected'), ) - const id = (record?.target as HTMLElement).getAttribute('data-note-id') + const id = (record?.target as HTMLElement)?.getAttribute('data-note-id') ?? null if (id) { this.selectedNoteId = id } @@ -91,7 +95,9 @@ export class OmnisearchModal extends SuggestModal { } onClose(): void { - this.mutationObserver.disconnect() + if (this.mutationObserver) { + this.mutationObserver.disconnect() + } } async getSuggestions(query: string): Promise { @@ -102,47 +108,55 @@ export class OmnisearchModal extends SuggestModal { prefix: true, fuzzy: term => (term.length > 4 ? 0.2 : false), combineWith: 'AND', - boost: { basename: 2, title: 1.5 }, + boost: { basename: 2, headings1: 1.5, headings2: 1.3, headings3: 1.1 }, }) .sort((a, b) => b.score - a.score) .slice(0, 50) - // console.log('Omnisearch - Results:') + // console.log(`Omnisearch - Results for "${query}"`) // console.log(results) - return results.map(result => { - const note = this.plugin.indexedNotes[result.id] - let content = note.content - let basename = note.basename + const suggestions = await Promise.all( + results.map(async result => { + const file = this.app.vault.getAbstractFileByPath(result.id) as TFile + // const metadata = this.app.metadataCache.getFileCache(file) + let content = (await this.app.vault.cachedRead(file)).toLowerCase() + let basename = file.basename - // If the body contains a searched term, find its position - // and trim the text around it - const pos = content.toLowerCase().indexOf(result.terms[0]) - const surroundLen = 180 - if (pos > -1) { - const from = Math.max(0, pos - surroundLen) - const to = Math.min(content.length - 1, pos + surroundLen) - content = - (from > 0 ? '…' : '') + - content.slice(from, to).trim() + - (to < content.length - 1 ? '…' : '') - } + // Sort the terms from smaller to larger + // and highlight them in the title and body + const terms = result.terms.sort((a, b) => a.length - b.length) + const reg = new RegExp(terms.map(escapeRegex).join('|'), 'gi') + const matches = getAllIndexes(content, reg) - // Sort the terms from smaller to larger - // and highlight them in the title and body - const terms = result.terms.sort((a, b) => a.length - b.length) - const reg = new RegExp(terms.map(escapeRegex).join('|'), 'gi') - content = content.replace(reg, highlighter) - basename = basename.replace(reg, highlighter) + // If the body contains a searched term, find its position + // and trim the text around it + const pos = content.toLowerCase().indexOf(result.terms[0]) + const surroundLen = 180 + if (pos > -1) { + const from = Math.max(0, pos - surroundLen) + const to = Math.min(content.length - 1, pos + surroundLen) + content = + (from > 0 ? '…' : '') + + content.slice(from, to).trim() + + (to < content.length - 1 ? '…' : '') + } - const resultNote: ResultNote = { - content, - basename, - path: note.path, - keyword: result.terms[0], - occurence: 0, - } - return resultNote - }) + // console.log(matches) + content = content.replace(reg, highlighter) + basename = basename.replace(reg, highlighter) + + const resultNote: ResultNote = { + content, + basename, + path: file.path, + matches, + occurence: 0, + } + return resultNote + }), + ) + + return suggestions } renderSuggestion(value: ResultNote, el: HTMLElement): void { @@ -160,11 +174,18 @@ export class OmnisearchModal extends SuggestModal { async onChooseSuggestion(item: ResultNote): Promise { const file = this.app.vault.getAbstractFileByPath(item.path) as TFile + // const fileCache = this.app.metadataCache.getFileCache(file) + // console.log(fileCache) const content = (await this.app.vault.cachedRead(file)).toLowerCase() - const offset = content.indexOf(item.keyword.toLowerCase()) + const offset = content.indexOf( + item.matches[item.occurence].match.toLowerCase(), + ) await this.app.workspace.openLinkText(item.path, '') const view = this.app.workspace.getActiveViewOfType(MarkdownView) + if (!view) { + throw new Error('OmniSearch - No active MarkdownView') + } const pos = view.editor.offsetToPos(offset) pos.ch = 0 diff --git a/src/utils.ts b/src/utils.ts index c8e8243..b80293a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,12 @@ import markdownToTxt from 'markdown-to-txt' -import { regexLineSplit, regexWikilink, regexYaml } from './globals' +import { CachedMetadata } from 'obsidian' +import { + isSearchMatch, + regexLineSplit, + regexWikilink, + regexYaml, + SearchMatch, +} from './globals' export function highlighter(str: string): string { return '' + str + '' @@ -64,3 +71,25 @@ export function wait(ms: number): Promise { export function escapeRegex(str: string): string { return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') } + +/** + * Returns the positions of all occurences of `val` inside of `text` + * https://stackoverflow.com/a/58828841 + * @param text + * @param val + * @returns + */ +export function getAllIndexes(text: string, val: RegExp): SearchMatch[] { + return [...text.matchAll(val)] + .map(o => ({ match: o[0], index: o.index })) + .filter(isSearchMatch) +} + +export function extractHeadingsFromCache( + cache: CachedMetadata, + level: number, +): string[] { + return ( + cache.headings?.filter(h => h.level === level).map(h => h.heading) ?? [] + ) +} diff --git a/tsconfig.json b/tsconfig.json index 1383e2f..f70dba1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true,