Added filename in search fields, updated minisearch to beta, auto-search

This commit is contained in:
Simon Cambier
2022-04-09 23:05:15 +02:00
parent e48834a386
commit 8139045ef3
5 changed files with 100 additions and 63 deletions

View File

@@ -1,5 +1,13 @@
.osresult__title { .osresult__title {
/* font-size: var(--font-adaptive-normal); */ font-weight: bold;
}
.osresult__name {
padding-left: 1em;
font-size: small;
font-weight: normal;
/* color: var(--text-muted);
text-decoration: underline; */
} }
.osresult__body { .osresult__body {

View File

@@ -60,5 +60,7 @@ esbuild
logLevel: 'info', logLevel: 'info',
sourcemap: prod ? false : 'inline', sourcemap: prod ? false : 'inline',
treeShaking: true, treeShaking: true,
minify: prod,
legalComments: 'none',
}) })
.catch(() => process.exit(1)) .catch(() => process.exit(1))

View File

@@ -30,7 +30,7 @@
"typescript": "4.4.4" "typescript": "4.4.4"
}, },
"dependencies": { "dependencies": {
"minisearch": "^4.0.3", "minisearch": "^5.0.0-beta1",
"remove-markdown": "^0.3.0" "remove-markdown": "^0.3.0"
} }
} }

8
pnpm-lock.yaml generated
View File

@@ -12,7 +12,7 @@ specifiers:
eslint-plugin-import: 2.22.1 eslint-plugin-import: 2.22.1
eslint-plugin-node: 11.1.0 eslint-plugin-node: 11.1.0
eslint-plugin-promise: 5.0.0 eslint-plugin-promise: 5.0.0
minisearch: ^4.0.3 minisearch: ^5.0.0-beta1
obsidian: latest obsidian: latest
prettier: ^2.6.2 prettier: ^2.6.2
prettier-eslint: ^13.0.0 prettier-eslint: ^13.0.0
@@ -21,7 +21,7 @@ specifiers:
typescript: 4.4.4 typescript: 4.4.4
dependencies: dependencies:
minisearch: 4.0.3 minisearch: 5.0.0-beta1
remove-markdown: 0.3.0 remove-markdown: 0.3.0
devDependencies: devDependencies:
@@ -1540,8 +1540,8 @@ packages:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true dev: true
/minisearch/4.0.3: /minisearch/5.0.0-beta1:
resolution: {integrity: sha512-yEbNeb41Qk0g9IMJAgmp/kEaW2Ub2cs9MAKphTel24O8mymLheFU7esyfiumeUsvs2f4O0boO0zrp4bc9Po2cA==} resolution: {integrity: sha512-/huf6dMqE0182OzJPTIU+SyjY4HBds2CL82ADJboPsROiuFpnOz9wyCxWJwHX+fwehYS1hUAr87W5AGVcfn4jQ==}
dev: false dev: false
/mkdirp/0.5.6: /mkdirp/0.5.6:

View File

@@ -1,43 +1,48 @@
import { App, Plugin, SuggestModal, TFile } from 'obsidian' import { Plugin, SuggestModal } from 'obsidian'
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import removeMarkdown from 'remove-markdown' import removeMarkdown from 'remove-markdown'
type OmnisearchMatch = {
path: string
body: string
title: string
}
type Note = { type Note = {
path: string path: string
content: string name: string
title: string
body: string
} }
const regexWikilink = /\[\[(?<name>.+?)(\|(?<alias>.+?))?\]\]/g
const regexEmbed = /!\[\[.+?\]\]/g
export default class OmnisearchPlugin extends Plugin { export default class OmnisearchPlugin extends Plugin {
minisearch: MiniSearch<Note> minisearch: MiniSearch<Note>
files: TFile[] lastSearch?: string
contents: Record<string, string>
setupIndex(): void { async instantiateMinisearch(): Promise<void> {
this.minisearch = new MiniSearch<Note>({ this.minisearch = new MiniSearch<Note>({
idField: 'path', idField: 'path',
fields: ['content', 'title', 'path'], fields: ['body', 'title', 'name'],
// storeFields: ['path'], storeFields: ['body', 'title', 'name'],
}) })
const files = this.app.vault.getMarkdownFiles()
for (const file of files) {
// Fetch content from the cache,
// trim the markdown, remove embeds and clear wikilinks
const content = clearContent(await this.app.vault.cachedRead(file))
.replace(regexEmbed, '')
.replace(regexWikilink, (sub, name, sep, alias) => alias ?? name)
// Split the "title" (the first line/sentence) from the rest of the content
const title = getFirstLine(content)
const body = removeFirstLine(content)
// Index those fields inside Minisearch
this.minisearch.add({ title, body, path: file.path, name: file.name })
}
} }
async onload(): Promise<void> { async onload(): Promise<void> {
this.contents = {}
this.setupIndex()
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
this.files = this.app.vault.getMarkdownFiles() await this.instantiateMinisearch()
for (const file of this.files) {
const content = await this.app.vault.cachedRead(file)
this.contents[file.path] = clearContent(content) // truncateText(clearContent(content))
this.minisearch.add({ content, path: file.path })
}
}) })
this.addCommand({ this.addCommand({
@@ -51,13 +56,15 @@ export default class OmnisearchPlugin extends Plugin {
} }
} }
class OmnisearchModal extends SuggestModal<OmnisearchMatch> { class OmnisearchModal extends SuggestModal<Note> {
plugin: OmnisearchPlugin plugin: OmnisearchPlugin
constructor(plugin: OmnisearchPlugin) { constructor(plugin: OmnisearchPlugin) {
super(plugin.app) super(plugin.app)
this.plugin = plugin this.plugin = plugin
this.setPlaceholder('Type to search through your notes') this.setPlaceholder('Type to search through your notes')
this.setInstructions([ this.setInstructions([
{ command: '↑↓', purpose: 'to navigate' }, { command: '↑↓', purpose: 'to navigate' },
{ command: '↵', purpose: 'to open' }, { command: '↵', purpose: 'to open' },
@@ -67,71 +74,95 @@ class OmnisearchModal extends SuggestModal<OmnisearchMatch> {
]) ])
} }
getSuggestions(query: string): OmnisearchMatch[] { onOpen(): void {
this.inputEl.focus()
if (this.plugin.lastSearch) {
const event = new Event('input', {
bubbles: true,
cancelable: true,
})
this.inputEl.value = this.plugin.lastSearch
this.inputEl.dispatchEvent(event)
this.inputEl.select()
}
}
getSuggestions(query: string): Note[] {
console.log('query: ' + query)
this.plugin.lastSearch = query
const results = this.plugin.minisearch const results = this.plugin.minisearch
.search(query, { .search(query, {
prefix: true, prefix: true,
fuzzy: term => (term.length > 4 ? 0.2 : false), fuzzy: term => (term.length > 4 ? 0.2 : false),
combineWith: 'AND', combineWith: 'AND',
// processTerm: term => term.length <= minLength ? false : term, boost: { name: 2, title: 1.5 },
boost: { title: 2 },
}) })
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
.slice(0, 50)
console.log(results)
return results.map(result => { return results.map(result => {
const file = this.plugin.files.find(f => f.path === result.id) // result.id == the file's path
let title = getFirstLine(this.plugin.contents[file.path]) let name = result.name
let body = removeFirstLine(this.plugin.contents[file.path]) let title = result.title
let body = result.body
// Highlight the words
const highlight = (str: string): string =>
'<span class="search-result-file-matched-text">' + str + '</span>'
// If the body contains a searched term, find its position
// and trim the text around it
const pos = body.toLowerCase().indexOf(result.terms[0]) const pos = body.toLowerCase().indexOf(result.terms[0])
const surroundLen = 200
if (pos > -1) { if (pos > -1) {
const from = Math.max(0, pos - 150) const from = Math.max(0, pos - surroundLen)
const to = Math.min(body.length - 1, pos + 150) const to = Math.min(body.length - 1, pos + surroundLen)
body = body =
(from > 0 ? '…' : '') + (from > 0 ? '…' : '') +
body.slice(from, to).trim() + body.slice(from, to).trim() +
(to < body.length - 1 ? '…' : '') (to < body.length - 1 ? '…' : '')
} }
result.terms // Sort the terms from smaller to larger
.sort((a, b) => a.length - b.length) // and highlight them in the title and body
.forEach(term => { const terms = result.terms.sort((a, b) => a.length - b.length)
term = term.toLowerCase() const reg = new RegExp(terms.join('|'), 'gi')
body = body.replace(reg, highlighter)
term = term.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') title = title.replace(reg, highlighter)
const reg = new RegExp(term, 'gi') name = name.replace(reg, highlighter)
body = body.replace(reg, highlight)
title = title.replace(reg, highlight)
})
return { return {
path: file.path, path: result.id,
name,
title, title,
body, body,
} }
}) })
} }
renderSuggestion(value: OmnisearchMatch, el: HTMLElement): void { renderSuggestion(value: Note, el: HTMLElement): void {
// title
const title = el.createEl('div', { cls: 'osresult__title' }) const title = el.createEl('div', { cls: 'osresult__title' })
title.innerHTML = value.title title.innerHTML = value.title
// filename
const name = el.createEl('span', { cls: 'osresult__name' })
name.innerHTML = value.name
title.appendChild(name)
// body
const body = el.createEl('div', { cls: 'osresult__body' }) const body = el.createEl('div', { cls: 'osresult__body' })
body.innerHTML = value.body body.innerHTML = value.body
} }
onChooseSuggestion( onChooseSuggestion(item: Note, evt: MouseEvent | KeyboardEvent): void {
item: OmnisearchMatch,
evt: MouseEvent | KeyboardEvent,
): void {
// this.app.workspace // this.app.workspace
this.app.workspace.openLinkText(item.path, '') this.app.workspace.openLinkText(item.path, '')
} }
} }
function highlighter(str: string): string {
return '<span class="search-result-file-matched-text">' + str + '</span>'
}
/** /**
* Strips the markdown and frontmatter * Strips the markdown and frontmatter
* @param text * @param text
@@ -161,12 +192,8 @@ function removeFirstLine(text: string): string {
return lines.join('\n') return lines.join('\n')
} }
function truncateText(text: string, len = 500): string {
return text.substring(0, len) + (text.length > 0 ? '...' : '')
}
function splitLines(text: string): string[] { function splitLines(text: string): string[] {
return text.split(/\r?\n|\r|\./) return text.split(/\r?\n|\r|(\. )/)
} }
function removeFrontMatter(text: string): string { function removeFrontMatter(text: string): string {