Svelte rewrite: search and display results
This commit is contained in:
19
src/CmpInput.svelte
Normal file
19
src/CmpInput.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick } from 'svelte'
|
||||||
|
import { searchQuery } from './stores'
|
||||||
|
|
||||||
|
let input: HTMLInputElement
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await tick()
|
||||||
|
input.focus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={input}
|
||||||
|
bind:value={$searchQuery}
|
||||||
|
type="text"
|
||||||
|
class="prompt-input"
|
||||||
|
placeholder="Type to search through your notes"
|
||||||
|
/>
|
||||||
119
src/CmpModal.svelte
Normal file
119
src/CmpModal.svelte
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
import type { TFile } from 'obsidian'
|
||||||
|
import CmpInput from './CmpInput.svelte'
|
||||||
|
import CmpNoteResult from './CmpNoteResult.svelte'
|
||||||
|
import type { ResultNote } from './globals'
|
||||||
|
import type OmnisearchPlugin from './main'
|
||||||
|
import { resultNotes, searchQuery, selectedNoteId } from './stores'
|
||||||
|
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from './utils'
|
||||||
|
|
||||||
|
export let plugin: OmnisearchPlugin
|
||||||
|
|
||||||
|
searchQuery.subscribe(q => {
|
||||||
|
getSuggestions(q).then(results => {
|
||||||
|
resultNotes.set(results)
|
||||||
|
if (results.length) {
|
||||||
|
selectedNoteId.set(results[0].path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getSuggestions(query: string): Promise<ResultNote[]> {
|
||||||
|
const results = plugin.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 = await Promise.all(
|
||||||
|
results.map(async result => {
|
||||||
|
const file = plugin.app.vault.getAbstractFileByPath(result.id) as TFile
|
||||||
|
// const metadata = this.app.metadataCache.getFileCache(file)
|
||||||
|
let content = escapeHTML(
|
||||||
|
await plugin.app.vault.cachedRead(file),
|
||||||
|
).toLowerCase()
|
||||||
|
let basename = file.basename
|
||||||
|
|
||||||
|
// 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: file.path,
|
||||||
|
matches,
|
||||||
|
occurence: 0,
|
||||||
|
}
|
||||||
|
return resultNote
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CmpInput />
|
||||||
|
|
||||||
|
<div class="prompt-results">
|
||||||
|
{#each $resultNotes as result (result.path)}
|
||||||
|
<CmpNoteResult
|
||||||
|
selected={result.path === $selectedNoteId}
|
||||||
|
id={result.path}
|
||||||
|
title={result.basename}
|
||||||
|
content={result.content}
|
||||||
|
nbMatches={result.matches.length}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="prompt-instructions">
|
||||||
|
<div class="prompt-instruction">
|
||||||
|
<span class="prompt-instruction-command">↑↓</span><span>to navigate</span>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-instruction">
|
||||||
|
<span class="prompt-instruction-command">↵</span><span>to open</span>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-instruction">
|
||||||
|
<span class="prompt-instruction-command">ctrl ↵</span><span
|
||||||
|
>to open in a new pane</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-instruction">
|
||||||
|
<span class="prompt-instruction-command">shift ↵</span><span>to create</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-instruction">
|
||||||
|
<span class="prompt-instruction-command">esc</span><span>to dismiss</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let title = ''
|
export let title = ''
|
||||||
export let content = ''
|
export let content = ''
|
||||||
export let id = ''
|
export let id = ''
|
||||||
export let nbMatches = 0
|
export let nbMatches = 0
|
||||||
|
export let selected = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div data-note-id={id} class="omnisearch-result">
|
<div data-note-id={id} class="suggestion-item omnisearch-result"
|
||||||
|
class:is-selected="{selected}">
|
||||||
<span class="omnisearch-result__title">
|
<span class="omnisearch-result__title">
|
||||||
{@html title}
|
{@html title}
|
||||||
</span>
|
</span>
|
||||||
<span class="omnisearch-result__counter">
|
<span class="omnisearch-result__counter">
|
||||||
{nbMatches}
|
{nbMatches} {nbMatches > 1 ? 'matches' : 'match'}
|
||||||
{#if nbMatches > 1}matches{:else}match{/if}
|
|
||||||
</span>
|
</span>
|
||||||
<div class="omnisearch-result__body">
|
<div class="omnisearch-result__body">
|
||||||
{@html content}
|
{@html content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
218
src/modal.ts
218
src/modal.ts
@@ -1,62 +1,98 @@
|
|||||||
import { MarkdownView, SuggestModal, TFile } from 'obsidian'
|
import { MarkdownView, Modal, TFile } from 'obsidian'
|
||||||
import type { ResultNote } from './globals'
|
import type { ResultNote } from './globals'
|
||||||
import type OmnisearchPlugin from './main'
|
import type OmnisearchPlugin from './main'
|
||||||
import CmpNoteResult from './CmpNoteResult.svelte'
|
import CmpNoteResult from './CmpNoteResult.svelte'
|
||||||
|
import CmpModal from './CmpModal.svelte'
|
||||||
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from './utils'
|
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from './utils'
|
||||||
import { selectedNoteId } from './store'
|
import { selectedNoteId } from './stores'
|
||||||
import { get } from 'svelte/store'
|
|
||||||
|
|
||||||
export class OmnisearchModal extends SuggestModal<ResultNote> {
|
export class OmnisearchModal extends Modal {
|
||||||
private plugin: OmnisearchPlugin
|
private plugin: OmnisearchPlugin
|
||||||
private mutationObserver?: MutationObserver
|
private mutationObserver?: MutationObserver
|
||||||
|
private cmp: CmpModal
|
||||||
|
|
||||||
constructor(plugin: OmnisearchPlugin) {
|
constructor(plugin: OmnisearchPlugin) {
|
||||||
super(plugin.app)
|
super(plugin.app)
|
||||||
this.plugin = plugin
|
this.plugin = plugin
|
||||||
|
this.modalEl.addClass('omnisearch-modal', 'prompt')
|
||||||
|
this.modalEl.replaceChildren() // Remove all the default Modal's children
|
||||||
|
|
||||||
this.modalEl.addClass('omnisearch-modal')
|
this.cmp = new CmpModal({
|
||||||
|
target: this.modalEl,
|
||||||
|
props: {
|
||||||
|
plugin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.setPlaceholder('Type to search through your notes')
|
// this.modalEl.addClass('omnisearch-modal')
|
||||||
|
|
||||||
this.setInstructions([
|
// this.setPlaceholder('Type to search through your notes')
|
||||||
{ command: '↑↓', purpose: 'to navigate' },
|
|
||||||
{ command: '↵', purpose: 'to open' },
|
// this.setInstructions([
|
||||||
{ command: 'ctrl ↵', purpose: 'to open in a new pane' },
|
// { command: '↑↓', purpose: 'to navigate' },
|
||||||
{ command: 'shift ↵', purpose: 'to create' },
|
// { command: '↵', purpose: 'to open' },
|
||||||
{ command: 'esc', purpose: 'to dismiss' },
|
// { command: 'ctrl ↵', purpose: 'to open in a new pane' },
|
||||||
])
|
// { command: 'shift ↵', purpose: 'to create' },
|
||||||
|
// { command: 'esc', purpose: 'to dismiss' },
|
||||||
|
// ])
|
||||||
}
|
}
|
||||||
|
|
||||||
async onKeydown(ev: KeyboardEvent): Promise<void> {
|
onOpen(): void {
|
||||||
const noteId = get(selectedNoteId)
|
this.containerEl.style.border = '1px solid red'
|
||||||
if (ev.key !== 'Enter' || !noteId) return
|
this.modalEl.style.border = '1px solid blue'
|
||||||
|
this.contentEl.style.border = '1px solid green'
|
||||||
|
// this.inputEl.focus()
|
||||||
|
// this.inputEl.onkeydown = this.onKeydown.bind(this)
|
||||||
|
// Reload last search, if any
|
||||||
|
// 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()
|
||||||
|
// // this.inputEl.spellcheck = false
|
||||||
|
// }
|
||||||
|
|
||||||
if (ev.ctrlKey || ev.metaKey) {
|
// this.setupObserver(this.modalEl)
|
||||||
// Open in a new pane
|
|
||||||
await this.app.workspace.openLinkText(noteId, '', true)
|
|
||||||
}
|
|
||||||
else if (ev.shiftKey) {
|
|
||||||
// Create a note
|
|
||||||
try {
|
|
||||||
const file = await this.app.vault.create(
|
|
||||||
this.inputEl.value + '.md',
|
|
||||||
'# ' + this.inputEl.value,
|
|
||||||
)
|
|
||||||
await this.app.workspace.openLinkText(file.path, '')
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// async onKeydown(ev: KeyboardEvent): Promise<void> {
|
||||||
|
// if (ev.key === 'ArrowRight') {
|
||||||
|
// console.log('TODO: open in-note search')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// const noteId = get(selectedNoteId)
|
||||||
|
// if (ev.key !== 'Enter' || !noteId) return
|
||||||
|
|
||||||
|
// if (ev.ctrlKey || ev.metaKey) {
|
||||||
|
// // Open in a new pane
|
||||||
|
// await this.app.workspace.openLinkText(noteId, '', true)
|
||||||
|
// }
|
||||||
|
// else if (ev.shiftKey) {
|
||||||
|
// // Create a note
|
||||||
|
// try {
|
||||||
|
// const file = await this.app.vault.create(
|
||||||
|
// this.inputEl.value + '.md',
|
||||||
|
// '# ' + this.inputEl.value,
|
||||||
|
// )
|
||||||
|
// await this.app.workspace.openLinkText(file.path, '')
|
||||||
|
// }
|
||||||
|
// catch (e) {
|
||||||
|
// 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()
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observes the modal element to keep track of which search result is currently selected
|
* Observes the modal element to keep track of which search result is currently selected
|
||||||
* @param modalEl
|
* @param modalEl
|
||||||
@@ -80,108 +116,6 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen(): void {
|
|
||||||
this.inputEl.focus()
|
|
||||||
this.inputEl.onkeydown = this.onKeydown.bind(this)
|
|
||||||
// Reload last search, if any
|
|
||||||
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()
|
|
||||||
this.inputEl.spellcheck = false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setupObserver(this.modalEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose(): void {
|
|
||||||
if (this.mutationObserver) {
|
|
||||||
this.mutationObserver.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSuggestions(query: string): Promise<ResultNote[]> {
|
|
||||||
this.plugin.lastSearch = query
|
|
||||||
|
|
||||||
const results = this.plugin.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 = 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 = escapeHTML(
|
|
||||||
await this.app.vault.cachedRead(file),
|
|
||||||
).toLowerCase()
|
|
||||||
let basename = file.basename
|
|
||||||
|
|
||||||
// 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: file.path,
|
|
||||||
matches,
|
|
||||||
occurence: 0,
|
|
||||||
}
|
|
||||||
return resultNote
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSuggestion(value: ResultNote, el: HTMLElement): void {
|
|
||||||
new CmpNoteResult({
|
|
||||||
target: el,
|
|
||||||
props: {
|
|
||||||
id: value.path,
|
|
||||||
title: value.basename,
|
|
||||||
content: value.content,
|
|
||||||
nbMatches: value.matches.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async onChooseSuggestion(item: ResultNote): Promise<void> {
|
async onChooseSuggestion(item: ResultNote): Promise<void> {
|
||||||
const file = this.app.vault.getAbstractFileByPath(item.path) as TFile
|
const file = this.app.vault.getAbstractFileByPath(item.path) as TFile
|
||||||
// const fileCache = this.app.metadataCache.getFileCache(file)
|
// const fileCache = this.app.metadataCache.getFileCache(file)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { writable } from 'svelte/store'
|
|
||||||
|
|
||||||
export const selectedNoteId = writable<string>()
|
|
||||||
6
src/stores.ts
Normal file
6
src/stores.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import type { ResultNote } from './globals'
|
||||||
|
|
||||||
|
export const selectedNoteId = writable<string>('')
|
||||||
|
export const searchQuery = writable<string>('')
|
||||||
|
export const resultNotes = writable<ResultNote[]>([])
|
||||||
Reference in New Issue
Block a user