diff --git a/.eslintrc.js b/.eslintrc.js
index 09e6c2d..5bf7b7a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -34,7 +34,7 @@ module.exports = {
// 'simple-import-sort/exports': 'warn',
'@typescript-eslint/func-call-spacing': ['error'],
'@typescript-eslint/explicit-function-return-type': [
- 'error',
+ 'warn',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
diff --git a/src/CmpInput.svelte b/src/CmpInput.svelte
index 2d58aca..4304734 100644
--- a/src/CmpInput.svelte
+++ b/src/CmpInput.svelte
@@ -12,21 +12,16 @@ onMount(async () => {
input.select()
})
-selectedNote.subscribe((note) => {
- const elem = document.querySelector(`[data-note-id="${note?.path}"]`)
- elem?.scrollIntoView({ behavior: "auto", block: "nearest" })
-})
-
// const throttledMoveNoteSelection = throttle(moveNoteSelection, 75)
function moveNoteSelection(ev: KeyboardEvent): void {
switch (ev.key) {
case "ArrowDown":
- selectedNote.next()
ev.preventDefault()
+ selectedNote.next()
break
case "ArrowUp":
- selectedNote.previous()
ev.preventDefault()
+ selectedNote.previous()
break
case "ArrowLeft":
break
@@ -34,6 +29,7 @@ function moveNoteSelection(ev: KeyboardEvent): void {
break
case "Enter":
+ ev.preventDefault()
if (ev.ctrlKey || ev.metaKey) {
// Open in a new pane
dispatch("ctrl-enter", $selectedNote)
@@ -46,6 +42,14 @@ function moveNoteSelection(ev: KeyboardEvent): void {
}
break
}
+
+ // Scroll selected note into view when selecting with keyboard
+ if ($selectedNote) {
+ const elem = document.querySelector(
+ `[data-note-id="${$selectedNote.path}"]`
+ )
+ elem?.scrollIntoView({ behavior: "auto", block: "nearest" })
+ }
}
diff --git a/src/CmpModal.svelte b/src/CmpModal.svelte
index eebe4e5..26d06f5 100644
--- a/src/CmpModal.svelte
+++ b/src/CmpModal.svelte
@@ -1,16 +1,17 @@
diff --git a/src/CmpNoteResult.svelte b/src/CmpNoteResult.svelte
index 6d4dcb9..778113f 100644
--- a/src/CmpNoteResult.svelte
+++ b/src/CmpNoteResult.svelte
@@ -1,14 +1,35 @@
{@html note.basename}
@@ -17,6 +38,6 @@ export let note: ResultNote
{note.matches.length} {note.matches.length > 1 ? "matches" : "match"}
- {@html note.content}
+ {@html cleanContent}
diff --git a/src/main.ts b/src/main.ts
index 1d583d7..0de27af 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,16 +1,18 @@
-import { Notice, Plugin, TAbstractFile, TFile } from 'obsidian'
-import MiniSearch from 'minisearch'
-import type { IndexedNote } from './globals'
-import { escapeHTML, extractHeadingsFromCache, wait } from './utils'
+import { Plugin, TFile } from 'obsidian'
import { OmnisearchModal } from './modal'
+import { plugin } from './stores'
+import {
+ addToIndex,
+ instantiateMinisearch,
+ removeFromIndex,
+ removeFromIndexByPath,
+} from './search'
export default class OmnisearchPlugin extends Plugin {
- minisearch!: MiniSearch
- lastSearch?: string
- indexedNotes: Record = {}
-
async onload(): Promise {
- await this.instantiateMinisearch()
+ plugin.set(this)
+
+ await instantiateMinisearch()
// Commands to display Omnisearch modal
this.addCommand({
@@ -25,114 +27,27 @@ export default class OmnisearchPlugin extends Plugin {
// Listeners to keep the search index up-to-date
this.registerEvent(
this.app.vault.on('create', file => {
- this.addToIndex(file)
+ addToIndex(file)
}),
)
this.registerEvent(
this.app.vault.on('delete', file => {
- this.removeFromIndex(file)
+ removeFromIndex(file)
}),
)
this.registerEvent(
this.app.vault.on('modify', async file => {
- this.removeFromIndex(file)
- await this.addToIndex(file)
+ removeFromIndex(file)
+ await addToIndex(file)
}),
)
this.registerEvent(
this.app.vault.on('rename', async (file, oldPath) => {
if (file instanceof TFile && file.path.endsWith('.md')) {
- this.removeFromIndexByPath(oldPath)
- await this.addToIndex(file)
+ removeFromIndexByPath(oldPath)
+ await addToIndex(file)
}
}),
)
}
-
- async instantiateMinisearch(): Promise {
- this.indexedNotes = {}
- this.minisearch = new MiniSearch({
- idField: 'path',
- fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'],
- })
-
- // Index files that are already present
- const start = new Date().getTime()
- const files = this.app.vault.getMarkdownFiles()
-
- // This is basically the same behavior as MiniSearch's `addAllAsync()`.
- // We index files by batches of 10
- if (files.length) {
- console.log('Omnisearch - indexing ' + files.length + ' files')
- }
- for (let i = 0; i < files.length; ++i) {
- if (i % 10 === 0) await wait(0)
- const file = files[i]
- // console.log(file.path)
- await this.addToIndex(file)
- }
-
- if (files.length > 0) {
- new Notice(
- `Omnisearch - Indexed ${files.length} notes in ${
- new Date().getTime() - start
- }ms`,
- )
- }
- }
-
- async addToIndex(file: TAbstractFile): Promise {
- 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`)
- }
- // Fetch content from the cache,
- // trim the markdown, remove embeds and clear wikilinks
- const content = escapeHTML(await this.app.vault.cachedRead(file))
-
- // Purge HTML before indexing
- const tmp = document.createElement('div')
- tmp.innerHTML = content
-
- // Make the document and index it
- const note: IndexedNote = {
- basename: file.basename,
- content: tmp.innerText, // content,
- 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
- }
- catch (e) {
- console.trace('Error while indexing ' + file.basename)
- console.error(e)
- }
- }
-
- removeFromIndex(file: TAbstractFile): void {
- if (file instanceof TFile && file.path.endsWith('.md')) {
- // console.log(`Omnisearch - removing ${file.path} from index`)
- return this.removeFromIndexByPath(file.path)
- }
- }
-
- removeFromIndexByPath(path: string): void {
- const note = this.indexedNotes[path]
- this.minisearch.remove(note)
- delete this.indexedNotes[path]
- }
}
diff --git a/src/modal.ts b/src/modal.ts
index 9ea07e4..faf1629 100644
--- a/src/modal.ts
+++ b/src/modal.ts
@@ -1,25 +1,18 @@
-import { Modal } from "obsidian";
-import type OmnisearchPlugin from "./main";
-import CmpModal from "./CmpModal.svelte";
+import { Modal } from 'obsidian'
+import type OmnisearchPlugin from './main'
+import CmpModal from './CmpModal.svelte'
+import { modal } from './stores'
export class OmnisearchModal extends Modal {
constructor(plugin: OmnisearchPlugin) {
- super(plugin.app);
- this.modalEl.addClass("omnisearch-modal", "prompt");
- this.modalEl.replaceChildren(); // Remove all the default Modal's children
+ super(plugin.app)
+ this.modalEl.addClass('omnisearch-modal', 'prompt')
+ this.modalEl.replaceChildren() // Remove all the default Modal's children
+
+ modal.set(this)
new CmpModal({
target: this.modalEl,
- props: {
- modal: this,
- plugin,
- },
- });
- }
-
- onOpen(): void {
- // this.containerEl.style.border = '1px solid red'
- // this.modalEl.style.border = '1px solid blue'
- // this.contentEl.style.border = '1px solid green'
+ })
}
}
diff --git a/src/notes.ts b/src/notes.ts
new file mode 100644
index 0000000..b45ac83
--- /dev/null
+++ b/src/notes.ts
@@ -0,0 +1,32 @@
+import { MarkdownView, TFile } from 'obsidian'
+import { get } from 'svelte/store'
+import type { ResultNote } from './globals'
+import { plugin } from './stores'
+
+export async function openNote(
+ item: ResultNote,
+ newPane = false,
+): Promise {
+ const app = get(plugin).app
+ const file = app.vault.getAbstractFileByPath(item.path) as TFile
+ // const fileCache = app.metadataCache.getFileCache(file)
+ // console.log(fileCache)
+ const content = (await app.vault.cachedRead(file)).toLowerCase()
+ const offset = content.indexOf(
+ item.matches[item.occurence].match.toLowerCase(),
+ )
+ await app.workspace.openLinkText(item.path, '', newPane)
+
+ const view = app.workspace.getActiveViewOfType(MarkdownView)
+ if (!view) {
+ throw new Error('OmniSearch - No active MarkdownView')
+ }
+ const pos = view.editor.offsetToPos(offset)
+ pos.ch = 0
+
+ view.editor.setCursor(pos)
+ view.editor.scrollIntoView({
+ from: { line: pos.line - 10, ch: 0 },
+ to: { line: pos.line + 10, ch: 0 },
+ })
+}
diff --git a/src/search.ts b/src/search.ts
new file mode 100644
index 0000000..b0a78d9
--- /dev/null
+++ b/src/search.ts
@@ -0,0 +1,162 @@
+import { Notice, TFile, type TAbstractFile } from 'obsidian'
+import MiniSearch from 'minisearch'
+import type { IndexedNote, ResultNote } from './globals'
+import { indexedNotes, plugin } from './stores'
+import { get } from 'svelte/store'
+import {
+ escapeHTML,
+ escapeRegex,
+ extractHeadingsFromCache,
+ getAllIndexes,
+ highlighter,
+ wait,
+} from './utils'
+
+let minisearch: MiniSearch
+
+export async function instantiateMinisearch(): Promise {
+ indexedNotes.set({})
+ minisearch = new MiniSearch({
+ idField: 'path',
+ fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'],
+ })
+
+ // Index files that are already present
+ const start = new Date().getTime()
+ const files = get(plugin).app.vault.getMarkdownFiles()
+
+ // This is basically the same behavior as MiniSearch's `addAllAsync()`.
+ // We index files by batches of 10
+ if (files.length) {
+ console.log('Omnisearch - indexing ' + files.length + ' files')
+ }
+ for (let i = 0; i < files.length; ++i) {
+ if (i % 10 === 0) await wait(0)
+ await addToIndex(files[i])
+ }
+
+ if (files.length > 0) {
+ new Notice(
+ `Omnisearch - Indexed ${files.length} notes in ${
+ new Date().getTime() - start
+ }ms`,
+ )
+ }
+}
+
+export function getSuggestions(query: string): ResultNote[] {
+ const results = minisearch
+ .search(query, {
+ prefix: true,
+ fuzzy: term => (term.length > 4 ? 0.2 : false),
+ combineWith: 'AND',
+ 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 for "${query}"`)
+ // console.log(results)
+
+ const suggestions = results.map(result => {
+ const note = indexedNotes.get(result.id)
+ if (!note) {
+ throw new Error(`Note "${result.id}" not indexed`)
+ }
+ let basename = escapeHTML(note.basename)
+ let content = escapeHTML(note.content)
+
+ // 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)
+
+ // 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 ? '…' : '')
+ }
+
+ // console.log(matches)
+ content = content.replace(reg, highlighter)
+ basename = basename.replace(reg, highlighter)
+
+ const resultNote: ResultNote = {
+ content,
+ basename,
+ path: note.path,
+ matches,
+ occurence: 0,
+ }
+
+ return resultNote
+ })
+
+ return suggestions
+}
+
+export async function addToIndex(file: TAbstractFile): Promise {
+ if (!(file instanceof TFile) || file.extension !== 'md') return
+ try {
+ const app = get(plugin).app
+ // console.log(`Omnisearch - adding ${file.path} to index`)
+ const fileCache = app.metadataCache.getFileCache(file)
+ // console.log(fileCache)
+
+ if (indexedNotes.get(file.path)) {
+ throw new Error(`${file.basename} is already indexed`)
+ }
+
+ // Fetch content from the cache to index it as-is
+ const content = await app.vault.cachedRead(file)
+
+ // Make the document and index it
+ const note: IndexedNote = {
+ basename: file.basename,
+ content,
+ path: file.path,
+ headings1: fileCache
+ ? extractHeadingsFromCache(fileCache, 1).join(' ')
+ : '',
+ headings2: fileCache
+ ? extractHeadingsFromCache(fileCache, 2).join(' ')
+ : '',
+ headings3: fileCache
+ ? extractHeadingsFromCache(fileCache, 3).join(' ')
+ : '',
+ }
+ minisearch.add(note)
+ indexedNotes.add(note)
+ }
+ catch (e) {
+ console.trace('Error while indexing ' + file.basename)
+ console.error(e)
+ }
+}
+
+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 removeFromIndexByPath(path: string): void {
+ const note = indexedNotes.get(path)
+ if (note) {
+ minisearch.remove(note)
+ indexedNotes.remove(path)
+ }
+}
diff --git a/src/stores.ts b/src/stores.ts
index 7fd271c..fb3f620 100644
--- a/src/stores.ts
+++ b/src/stores.ts
@@ -1,12 +1,33 @@
import { get, writable } from 'svelte/store'
-import type { ResultNote } from './globals'
+import type { IndexedNote, ResultNote } from './globals'
+import type OmnisearchPlugin from './main'
+import type { OmnisearchModal } from './modal'
-// export const selectedNoteId = writable('')
-export const searchQuery = writable('')
-export const resultNotes = writable([])
+function createIndexedNotes() {
+ const { subscribe, set, update } = writable>({})
+ return {
+ subscribe,
+ set,
+ add(note: IndexedNote) {
+ update(notes => {
+ notes[note.path] = note
+ return notes
+ })
+ },
+ remove(path: string) {
+ update(notes => {
+ delete notes[path]
+ return notes
+ })
+ },
+ get(path: string): IndexedNote | undefined {
+ return get(indexedNotes)[path]
+ },
+ }
+}
function createSelectedNote() {
- const { subscribe, set, update } = writable(null)
+ const { subscribe, set, update } = writable(null)
return {
subscribe,
set,
@@ -31,4 +52,9 @@ function createSelectedNote() {
}
}
+export const searchQuery = writable('')
+export const resultNotes = writable([])
+export const plugin = writable()
+export const modal = writable()
export const selectedNote = createSelectedNote()
+export const indexedNotes = createIndexedNotes()