Updated flow

This commit is contained in:
Simon Cambier
2022-04-19 22:41:34 +02:00
parent dc32bcad5b
commit d5f7382a98
7 changed files with 59 additions and 85 deletions

View File

@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { debounce } from "obsidian" import { debounce } from "obsidian"
import { createEventDispatcher, onMount, tick } from "svelte" import { createEventDispatcher, onMount, tick } from "svelte"
import { searchQuery } from "./stores"
export let debouncedValue: string
let elInput: HTMLInputElement let elInput: HTMLInputElement
let inputValue: string let inputValue: string
@@ -10,11 +11,11 @@ const dispatch = createEventDispatcher()
onMount(async () => { onMount(async () => {
await tick() await tick()
elInput.focus() elInput.focus()
elInput.value = $searchQuery // elInput.value = $searchQuery
elInput.select() elInput.select()
}) })
const debouncedOnInput = debounce(() => ($searchQuery = inputValue), 100) const debouncedOnInput = debounce(() => (debouncedValue = inputValue), 100)
function moveNoteSelection(ev: KeyboardEvent): void { function moveNoteSelection(ev: KeyboardEvent): void {
switch (ev.key) { switch (ev.key) {
@@ -46,7 +47,7 @@ function moveNoteSelection(ev: KeyboardEvent): void {
} }
} }
</script> </script>
{inputValue} - {debouncedValue}
<input <input
bind:this={elInput} bind:this={elInput}
bind:value={inputValue} bind:value={inputValue}

View File

@@ -1,25 +1,29 @@
<script lang="ts"> <script lang="ts">
import CmpInput from "./CmpInput.svelte" import CmpInput from "./CmpInput.svelte"
import CmpResultInFile from "./CmpResultInFile.svelte" import CmpResultInFile from "./CmpResultInFile.svelte"
import { excerptAfter, type SearchMatch } from "./globals" import { excerptAfter, type ResultNote, type SearchMatch } from "./globals"
import { modal, plugin, resultNotes } from "./stores" import { modal, plugin } from "./stores"
import { loopIndex } from "./utils" import { loopIndex } from "./utils"
import { tick } from "svelte" import { tick } from "svelte"
import { MarkdownView } from "obsidian" import { MarkdownView } from "obsidian"
import CmpModalVault from "./CmpModalVault.svelte" import { getSuggestions } from "./search"
import { OmnisearchModal } from "./modal"
export let canGoBack = false export let canGoBack = false
export let singleFilePath = ""
let matches: SearchMatch[] = []
let groupedOffsets: number[] = [] let groupedOffsets: number[] = []
let selectedIndex = 0 let selectedIndex = 0
let searchQuery: string
let note: ResultNote | null = null
$: {
note = getSuggestions(searchQuery, { singleFilePath })[0] ?? null
selectedIndex = 0
scrollIntoView()
}
$: note = $resultNotes[0]
$: { $: {
if (note) { if (note) {
matches = note.matches const groups = getGroups(note.matches)
const groups = getGroups()
groupedOffsets = groups.map((group) => groupedOffsets = groups.map((group) =>
Math.round((group.first()!.offset + group.last()!.offset) / 2) Math.round((group.first()!.offset + group.last()!.offset) / 2)
) )
@@ -27,20 +31,11 @@ $: {
// console.log(groupedOffsets) // console.log(groupedOffsets)
} }
} }
$: {
if (canGoBack) {
$modal.onClose = () => {
if (canGoBack) {
new OmnisearchModal($plugin).open()
}
}
}
}
/** /**
* Group together close * Group together close
*/ */
function getGroups(): SearchMatch[][] { function getGroups(matches: SearchMatch[]): SearchMatch[][] {
const groups: SearchMatch[][] = [] const groups: SearchMatch[][] = []
let lastOffset = -1 let lastOffset = -1
while (true) { while (true) {
@@ -99,6 +94,7 @@ function openSelection(): void {
<div class="modal-title">Omnisearch - File</div> <div class="modal-title">Omnisearch - File</div>
<CmpInput <CmpInput
bind:debouncedValue={searchQuery}
on:enter={openSelection} on:enter={openSelection}
on:arrow-up={() => moveIndex(-1)} on:arrow-up={() => moveIndex(-1)}
on:arrow-down={() => moveIndex(1)} on:arrow-down={() => moveIndex(1)}

View File

@@ -1,26 +1,39 @@
<script lang="ts"> <script lang="ts">
import { TFile } from "obsidian" import { TFile } from "obsidian"
import { tick } from "svelte" import { onMount, tick } from "svelte"
import CmpInput from "./CmpInput.svelte" import CmpInput from "./CmpInput.svelte"
import CmpResultNote from "./CmpResultNote.svelte" import CmpResultNote from "./CmpResultNote.svelte"
import type { ResultNote } from "./globals" import type { ResultNote } from "./globals"
import { OmnisearchModal } from "./modal" import { OmnisearchModal } from "./modal"
import { openNote } from "./notes" import { openNote } from "./notes"
import { modal, plugin, resultNotes, searchQuery } from "./stores" import { getSuggestions } from "./search"
import { lastSearch, modal, plugin } from "./stores"
import { loopIndex } from "./utils" import { loopIndex } from "./utils"
let selectedIndex = 0 let selectedIndex = 0
$: selectedNote = $resultNotes[selectedIndex]! let searchQuery: string
let resultNotes: ResultNote[] = []
$: selectedNote = resultNotes[selectedIndex]!
searchQuery.subscribe((q) => { $: {
if (searchQuery) {
resultNotes = getSuggestions(searchQuery)
$lastSearch = searchQuery
}
selectedIndex = 0 selectedIndex = 0
scrollIntoView()
}
onMount(async () => {
await tick()
searchQuery = $lastSearch
}) })
async function createOrOpenNote(item: ResultNote): Promise<void> { async function createOrOpenNote(item: ResultNote): Promise<void> {
try { try {
const file = await $plugin.app.vault.create( const file = await $plugin.app.vault.create(
$searchQuery + ".md", searchQuery + ".md",
"# " + $searchQuery "# " + searchQuery
) )
await $plugin.app.workspace.openLinkText(file.path, "") await $plugin.app.workspace.openLinkText(file.path, "")
} catch (e) { } catch (e) {
@@ -65,7 +78,7 @@ function onInputAltEnter(): void {
} }
function moveIndex(dir: 1 | -1): void { function moveIndex(dir: 1 | -1): void {
selectedIndex = loopIndex(selectedIndex + dir, $resultNotes.length) selectedIndex = loopIndex(selectedIndex + dir, resultNotes.length)
scrollIntoView() scrollIntoView()
} }
@@ -83,6 +96,7 @@ function scrollIntoView(): void {
<div class="modal-title">Omnisearch - Vault</div> <div class="modal-title">Omnisearch - Vault</div>
<CmpInput <CmpInput
bind:debouncedValue={searchQuery}
on:enter={onInputEnter} on:enter={onInputEnter}
on:shift-enter={onInputShiftEnter} on:shift-enter={onInputShiftEnter}
on:ctrl-enter={onInputCtrlEnter} on:ctrl-enter={onInputCtrlEnter}
@@ -93,7 +107,7 @@ function scrollIntoView(): void {
<div class="modal-content"> <div class="modal-content">
<div class="prompt-results"> <div class="prompt-results">
{#each $resultNotes as result, i} {#each resultNotes as result, i}
<CmpResultNote <CmpResultNote
selected={i === selectedIndex} selected={i === selectedIndex}
note={result} note={result}

View File

@@ -32,11 +32,13 @@ export const isSearchMatch = (o: { offset?: number }): o is SearchMatch => {
} }
export type ResultNote = { export type ResultNote = {
// searchResult: SearchResult score: number
path: string path: string
basename: string basename: string
content: string content: string
foundWords: string[] foundWords: string[]
matches: SearchMatch[] matches: SearchMatch[]
occurence: number
} }
export const SPACE_OR_PUNCTUATION =
/[|\n\r -#%-*,-/:;?@[-\]_{}\u00A0\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u1680\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2000-\u200A\u2010-\u2029\u202F-\u2043\u2045-\u2051\u2053-\u205F\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u3000-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]+/u

View File

@@ -2,7 +2,7 @@ import { Modal, TFile } from 'obsidian'
import type OmnisearchPlugin from './main' import type OmnisearchPlugin from './main'
import CmpModalVault from './CmpModalVault.svelte' import CmpModalVault from './CmpModalVault.svelte'
import CmpModalInFile from './CmpModalInFile.svelte' import CmpModalInFile from './CmpModalInFile.svelte'
import { inFileSearch, modal } from './stores' import { modal } from './stores'
export class OmnisearchModal extends Modal { export class OmnisearchModal extends Modal {
constructor(plugin: OmnisearchPlugin, file?: TFile, canGoBack = false) { constructor(plugin: OmnisearchPlugin, file?: TFile, canGoBack = false) {
@@ -15,7 +15,6 @@ export class OmnisearchModal extends Modal {
this.modalEl.append(closeEl) this.modalEl.append(closeEl)
this.modalEl.addClass('omnisearch-modal', 'prompt') this.modalEl.addClass('omnisearch-modal', 'prompt')
inFileSearch.set(file ?? null)
modal.set(this) modal.set(this)
if (file) { if (file) {
@@ -23,6 +22,7 @@ export class OmnisearchModal extends Modal {
target: this.modalEl, target: this.modalEl,
props: { props: {
canGoBack, canGoBack,
singleFilePath: file.path,
}, },
}) })
} }

View File

@@ -1,13 +1,7 @@
import { Notice, TFile, type TAbstractFile } from 'obsidian' import { Notice, TFile, type TAbstractFile } from 'obsidian'
import MiniSearch, { type SearchResult } from 'minisearch' import MiniSearch, { type SearchResult } from 'minisearch'
import type { IndexedNote, ResultNote, SearchMatch } from './globals' import { SPACE_OR_PUNCTUATION, type IndexedNote, type ResultNote, type SearchMatch } from './globals'
import { import { indexedNotes, plugin } from './stores'
indexedNotes,
inFileSearch as singleFileSearch,
plugin,
resultNotes,
searchQuery,
} from './stores'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { extractHeadingsFromCache, stringsToRegex, wait } from './utils' import { extractHeadingsFromCache, stringsToRegex, wait } from './utils'
@@ -20,6 +14,7 @@ let minisearchInstance: MiniSearch<IndexedNote>
export async function initGlobalSearchIndex(): Promise<void> { export async function initGlobalSearchIndex(): Promise<void> {
indexedNotes.set({}) indexedNotes.set({})
minisearchInstance = new MiniSearch({ minisearchInstance = new MiniSearch({
tokenize: text => text.split(SPACE_OR_PUNCTUATION),
idField: 'path', idField: 'path',
fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'], fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'],
}) })
@@ -48,7 +43,7 @@ export async function initGlobalSearchIndex(): Promise<void> {
} }
// Listen to the query input to trigger a search // Listen to the query input to trigger a search
subscribeToQuery() // subscribeToQuery()
} }
/** /**
@@ -72,31 +67,6 @@ function search(query: string): SearchResult[] {
}) })
} }
/**
* Automatically re-trigger the search when the query or the
* inFileSearch changes
*/
function subscribeToQuery(): void {
singleFileSearch.subscribe(async file => {
triggerQuery(get(searchQuery))
})
searchQuery.subscribe(triggerQuery)
async function triggerQuery(q: string): Promise<void> {
// If we're in "single file" mode, the search results array
// will contain a single result, related to this file
const results = get(singleFileSearch)
? getSuggestions(q, { singleFile: get(singleFileSearch) })
: getSuggestions(q)
// console.log('Search results')
// console.log(results)
// Save the results in the store
resultNotes.set(results)
}
}
/** /**
* Parses a text against a regex, and returns the { string, offset } matches * Parses a text against a regex, and returns the { string, offset } matches
* @param text * @param text
@@ -123,7 +93,7 @@ export function getMatches(text: string, reg: RegExp): SearchMatch[] {
*/ */
export function getSuggestions( export function getSuggestions(
query: string, query: string,
options?: Partial<{ singleFile: TFile | null }>, options?: Partial<{ singleFilePath: string | null }>,
): ResultNote[] { ): ResultNote[] {
// Get the raw results // Get the raw results
let results = search(query) let results = search(query)
@@ -131,9 +101,8 @@ export function getSuggestions(
// Either keep the 50 first results, // Either keep the 50 first results,
// or the one corresponding to `singleFile` // or the one corresponding to `singleFile`
if (options?.singleFile) { if (options?.singleFilePath) {
const file = options.singleFile const result = results.find(r => r.id === options.singleFilePath)
const result = results.find(r => r.id === file.path)
if (result) results = [result] if (result) results = [result]
else results = [] else results = []
} }
@@ -150,8 +119,8 @@ export function getSuggestions(
const words = Object.keys(result.match) const words = Object.keys(result.match)
const matches = getMatches(note.content, stringsToRegex(words)) const matches = getMatches(note.content, stringsToRegex(words))
const resultNote: ResultNote = { const resultNote: ResultNote = {
score: result.score,
foundWords: words, foundWords: words,
occurence: 0,
matches, matches,
...note, ...note,
} }

View File

@@ -30,17 +30,7 @@ function createIndexedNotes() {
/** /**
* If this field is set, the search will be limited to the given file * If this field is set, the search will be limited to the given file
*/ */
export const inFileSearch = writable<TFile | null>(null) // export const inFileSearch = writable<TFile | null>(null)
/**
* The current search query
*/
export const searchQuery = writable<string>('')
/**
* The search results list, according to the current search query
*/
export const resultNotes = writable<ResultNote[]>([])
/** /**
* A reference to the plugin instance * A reference to the plugin instance
@@ -56,3 +46,5 @@ export const modal = writable<OmnisearchModal>()
* The entire list of indexed notes, constantly kept up-to-date. * The entire list of indexed notes, constantly kept up-to-date.
*/ */
export const indexedNotes = createIndexedNotes() export const indexedNotes = createIndexedNotes()
export const lastSearch = writable('')