Using 2 modals to display the different kinds of results

This commit is contained in:
Simon Cambier
2022-04-18 10:28:13 +02:00
parent cfa24b9617
commit 6cb113b87e
8 changed files with 218 additions and 81 deletions

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import type { IndexedNote, SearchMatch } from "./globals"
import { indexedNotes, inFileSearch } from "./stores"
import { escapeHTML } from "./utils";
export let match: SearchMatch
let note: IndexedNote | null = null
inFileSearch.subscribe((file) => {
if (file) {
note = indexedNotes.get(file.path) ?? null
}
})
function cleanContent(content: string): string {
const pos = match.offset ?? -1
if (pos > -1) {
const surroundLen = 180
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 ? "…" : "")
}
return escapeHTML(content)
}
</script>
<div class="suggestion-item omnisearch-result">
<div class="omnisearch-result__body">
{cleanContent(note?.content ?? '')}
</div>
</div>

View File

@@ -3,20 +3,19 @@ import { debounce } from "obsidian";
import { createEventDispatcher, onMount, tick } from "svelte"
import { searchQuery, selectedNote } from "./stores"
let input: HTMLInputElement
let elInput: HTMLInputElement
let inputValue: string
const dispatch = createEventDispatcher()
onMount(async () => {
await tick()
input.focus()
input.select()
input.value = $searchQuery
elInput.focus()
elInput.select()
elInput.value = $searchQuery
})
const debouncedOnInput = debounce(() => $searchQuery = inputValue, 100)
// const throttledMoveNoteSelection = throttle(moveNoteSelection, 75)
function moveNoteSelection(ev: KeyboardEvent): void {
switch (ev.key) {
case "ArrowDown":
@@ -63,7 +62,7 @@ function moveNoteSelection(ev: KeyboardEvent): void {
</script>
<input
bind:this={input}
bind:this={elInput}
bind:value={inputValue}
on:input={debouncedOnInput}
on:keydown={moveNoteSelection}

59
src/CmpModalFile.svelte Normal file
View File

@@ -0,0 +1,59 @@
<script lang="ts">
import { onMount, tick } from "svelte"
import CmpInput from "./CmpInput.svelte"
import CmpNoteInternalResult from "./CmpInfileResult.svelte"
import CmpNoteResult from "./CmpNoteResult.svelte"
import type { ResultNote } from "./globals"
import { openNote } from "./notes"
import { getSuggestions } from "./search"
import {
inFileSearch,
modal,
plugin,
resultNotes,
searchQuery,
selectedNote,
} from "./stores"
$: firstResult = $resultNotes[0]
function onInputEnter(event: CustomEvent<ResultNote>): void {
// console.log(event.detail)
// openNote(event.detail)
// $modal.close()
}
</script>
<div class="modal-title">Omnisearch - File</div>
<CmpInput on:enter={onInputEnter} />
<div class="modal-content">
<div class="prompt-results">
{#if firstResult}
{#each firstResult.matches as match}
<CmpNoteInternalResult {match} />
{/each}
{:else}
We found 0 result for your search here.
{/if}
</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>
</div>

View File

@@ -1,13 +1,9 @@
<script lang="ts">
import { onMount, tick } from "svelte"
import CmpInput from "./CmpInput.svelte"
import CmpNoteInternalResult from "./CmpNoteInternalResult.svelte"
import CmpNoteResult from "./CmpNoteResult.svelte"
import type { ResultNote } from "./globals"
import { openNote } from "./notes"
import { getSuggestions } from "./search"
import {
inFileSearch,
modal,
plugin,
resultNotes,
@@ -15,25 +11,7 @@ import {
selectedNote,
} from "./stores"
$: firstResult = $resultNotes[0]
searchQuery.subscribe(async (q) => {
// If we're in "single file" mode, the search results array
// will contain a single result, related to this file
const results = $inFileSearch
? getSuggestions(q, { singleFile: $inFileSearch })
: getSuggestions(q)
console.log(results)
// Search on the whole vault
resultNotes.set(results)
const firstResult = results[0]
if (firstResult) {
await tick()
selectedNote.set(firstResult)
}
})
async function createOrOpenNote(item: ResultNote): Promise<void> {
try {
@@ -69,30 +47,18 @@ function onInputShiftEnter(event: CustomEvent<ResultNote>): void {
}
</script>
<div class="modal-title">Omnisearch</div>
<div class="modal-title">Omnisearch - Vault</div>
<CmpInput
on:enter={onInputEnter}
on:shift-enter={onInputShiftEnter}
on:ctrl-enter={onInputCtrlEnter}
on:enter={onInputEnter}
on:shift-enter={onInputShiftEnter}
on:ctrl-enter={onInputCtrlEnter}
/>
<div class="modal-content">
<div class="prompt-results">
{#if $inFileSearch}
<!-- In-file results -->
{#if firstResult}
{#each firstResult.matches as match}
<CmpNoteInternalResult {match} />
{/each}
{:else}
We found 0 result for your search here.
{/if}
{:else}
<!-- Multi-files results -->
{#each $resultNotes as result}
<CmpNoteResult selected={result === $selectedNote} note={result} />
{/each}
{/if}
</div>
<div class="prompt-instructions">
<div class="prompt-instruction">

View File

@@ -1,11 +0,0 @@
<script lang="ts">
import type { SearchMatch } from "./globals"
export let match: SearchMatch
</script>
<div class="suggestion-item omnisearch-result">
<div class="omnisearch-result__body">
{JSON.stringify(match)}
</div>
</div>

View File

@@ -12,13 +12,10 @@ export default class OmnisearchPlugin extends Plugin {
async onload(): Promise<void> {
plugin.set(this)
await initGlobalSearchIndex()
// Commands to display Omnisearch modal
// Commands to display Omnisearch modals
this.addCommand({
id: 'show-modal',
name: 'Vault search',
// hotkeys: [{ modifiers: ['Mod'], key: 'o' }],
callback: () => {
new OmnisearchModal(this).open()
},
@@ -27,7 +24,6 @@ export default class OmnisearchPlugin extends Plugin {
this.addCommand({
id: 'show-modal-infile',
name: 'In-file search',
// hotkeys: [{ modifiers: ['Mod'], key: 'o' }],
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
if (view) {
@@ -65,5 +61,7 @@ export default class OmnisearchPlugin extends Plugin {
}
}),
)
initGlobalSearchIndex()
}
}

View File

@@ -1,11 +1,13 @@
import { Modal, TFile } from 'obsidian'
import type OmnisearchPlugin from './main'
import CmpModal from './CmpModal.svelte'
import CmpModalVault from './CmpModalVault.svelte'
import CmpModalFile from './CmpModalFile.svelte'
import { inFileSearch, modal } from './stores'
export class OmnisearchModal extends Modal {
constructor(plugin: OmnisearchPlugin, file?: TFile) {
super(plugin.app)
// Remove all the default modal's children (except the close button)
// so that we can more easily customize it
const closeEl = this.containerEl.find('.modal-close-button')
@@ -14,11 +16,17 @@ export class OmnisearchModal extends Modal {
this.modalEl.addClass('omnisearch-modal', 'prompt')
inFileSearch.set(file ?? null)
modal.set(this)
new CmpModal({
if (file) {
new CmpModalFile({
target: this.modalEl,
})
}
else {
new CmpModalVault({
target: this.modalEl,
})
}
}
}

View File

@@ -1,12 +1,24 @@
import { Notice, TFile, type TAbstractFile } from 'obsidian'
import MiniSearch, { type SearchResult } from 'minisearch'
import type { IndexedNote, ResultNote, SearchMatch } from './globals'
import { indexedNotes, plugin } from './stores'
import {
indexedNotes,
inFileSearch,
plugin,
resultNotes,
searchQuery,
selectedNote,
} from './stores'
import { get } from 'svelte/store'
import { extractHeadingsFromCache, stringsToRegex, wait } from './utils'
import { tick } from 'svelte'
let minisearchInstance: MiniSearch<IndexedNote>
/**
* Initializes the MiniSearch instance,
* and adds all the notes to the index
*/
export async function initGlobalSearchIndex(): Promise<void> {
indexedNotes.set({})
minisearchInstance = new MiniSearch({
@@ -36,19 +48,19 @@ export async function initGlobalSearchIndex(): Promise<void> {
}ms`,
)
}
// Listen to the query input to trigger a search
subscribeToQuery()
}
export function getMatches(text: string, reg: RegExp): SearchMatch[] {
let match: RegExpExecArray | null = null
const matches: SearchMatch[] = []
while ((match = reg.exec(text)) !== null) {
const m = match[0]
if (m) matches.push({ match: m, offset: match.index })
}
return matches
}
/**
* Searches the index for the given query,
* and returns an array of raw results
* @param query
* @returns
*/
function search(query: string): SearchResult[] {
if (!query) return []
return minisearchInstance.search(query, {
prefix: true,
fuzzy: term => (term.length > 4 ? 0.2 : false),
@@ -62,11 +74,65 @@ function search(query: string): SearchResult[] {
})
}
/**
* Subscribe to the searchQuery store,
* and automatically triggers a search when the query changes
*/
function subscribeToQuery(): void {
searchQuery.subscribe(async q => {
// If we're in "single file" mode, the search results array
// will contain a single result, related to this file
const results = get(inFileSearch)
? getSuggestions(q, { singleFile: get(inFileSearch) })
: getSuggestions(q)
console.log(results)
// Save the results in the store
resultNotes.set(results)
// Automatically select the first result
const firstResult = results[0]
if (firstResult) {
await tick()
selectedNote.set(firstResult)
}
})
}
/**
* Parses a text against a regex, and returns the { string, offset } matches
* @param text
* @param reg
* @returns
*/
export function getMatches(text: string, reg: RegExp): SearchMatch[] {
let match: RegExpExecArray | null = null
const matches: SearchMatch[] = []
while ((match = reg.exec(text)) !== null) {
const m = match[0]
if (m) matches.push({ match: m, offset: match.index })
}
return matches
}
/**
* Searches the index, and returns an array of ResultNote objects.
* If we have the singleFile option set,
* the array contains a single result from that file
* @param query
* @param options
* @returns
*/
export function getSuggestions(
query: string,
options?: Partial<{ singleFile: TFile }>,
options?: Partial<{ singleFile: TFile | null }>,
): ResultNote[] {
// Get the raw results
let results = search(query)
if (!results.length) return []
// Either keep the 50 first results,
// or the one corresponding to `singleFile`
if (options?.singleFile) {
const file = options.singleFile
const result = results.find(r => r.id === file.path)
@@ -77,6 +143,7 @@ export function getSuggestions(
results = results.sort((a, b) => b.score - a.score).slice(0, 50)
}
// Map the raw results to get usable suggestions
const suggestions = results.map(result => {
const note = indexedNotes.get(result.id)
if (!note) {
@@ -96,8 +163,15 @@ export function getSuggestions(
return suggestions
}
/**
* Adds a file to the index
* @param file
* @returns
*/
export async function addToIndex(file: TAbstractFile): Promise<void> {
if (!(file instanceof TFile) || file.extension !== 'md') return
if (!(file instanceof TFile) || file.extension !== 'md') {
return
}
try {
const app = get(plugin).app
// console.log(`Omnisearch - adding ${file.path} to index`)
@@ -135,6 +209,11 @@ export async function addToIndex(file: TAbstractFile): Promise<void> {
}
}
/**
* Removes a file from the index
* @param file
* @returns
*/
export function removeFromIndex(file: TAbstractFile): void {
if (file instanceof TFile && file.path.endsWith('.md')) {
// console.log(`Omnisearch - removing ${file.path} from index`)
@@ -142,6 +221,10 @@ export function removeFromIndex(file: TAbstractFile): void {
}
}
/**
* Removes a file from the index, by its path
* @param path
*/
export function removeFromIndexByPath(path: string): void {
const note = indexedNotes.get(path)
if (note) {