Updated flow
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { debounce } from "obsidian"
|
||||
import { createEventDispatcher, onMount, tick } from "svelte"
|
||||
import { searchQuery } from "./stores"
|
||||
|
||||
export let debouncedValue: string
|
||||
|
||||
let elInput: HTMLInputElement
|
||||
let inputValue: string
|
||||
@@ -10,11 +11,11 @@ const dispatch = createEventDispatcher()
|
||||
onMount(async () => {
|
||||
await tick()
|
||||
elInput.focus()
|
||||
elInput.value = $searchQuery
|
||||
// elInput.value = $searchQuery
|
||||
elInput.select()
|
||||
})
|
||||
|
||||
const debouncedOnInput = debounce(() => ($searchQuery = inputValue), 100)
|
||||
const debouncedOnInput = debounce(() => (debouncedValue = inputValue), 100)
|
||||
|
||||
function moveNoteSelection(ev: KeyboardEvent): void {
|
||||
switch (ev.key) {
|
||||
@@ -46,7 +47,7 @@ function moveNoteSelection(ev: KeyboardEvent): void {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{inputValue} - {debouncedValue}
|
||||
<input
|
||||
bind:this={elInput}
|
||||
bind:value={inputValue}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<script lang="ts">
|
||||
import CmpInput from "./CmpInput.svelte"
|
||||
import CmpResultInFile from "./CmpResultInFile.svelte"
|
||||
import { excerptAfter, type SearchMatch } from "./globals"
|
||||
import { modal, plugin, resultNotes } from "./stores"
|
||||
import { excerptAfter, type ResultNote, type SearchMatch } from "./globals"
|
||||
import { modal, plugin } from "./stores"
|
||||
import { loopIndex } from "./utils"
|
||||
import { tick } from "svelte"
|
||||
import { MarkdownView } from "obsidian"
|
||||
import CmpModalVault from "./CmpModalVault.svelte"
|
||||
import { OmnisearchModal } from "./modal"
|
||||
import { getSuggestions } from "./search"
|
||||
|
||||
export let canGoBack = false
|
||||
|
||||
let matches: SearchMatch[] = []
|
||||
export let singleFilePath = ""
|
||||
let groupedOffsets: number[] = []
|
||||
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) {
|
||||
matches = note.matches
|
||||
const groups = getGroups()
|
||||
const groups = getGroups(note.matches)
|
||||
groupedOffsets = groups.map((group) =>
|
||||
Math.round((group.first()!.offset + group.last()!.offset) / 2)
|
||||
)
|
||||
@@ -27,20 +31,11 @@ $: {
|
||||
// console.log(groupedOffsets)
|
||||
}
|
||||
}
|
||||
$: {
|
||||
if (canGoBack) {
|
||||
$modal.onClose = () => {
|
||||
if (canGoBack) {
|
||||
new OmnisearchModal($plugin).open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group together close
|
||||
*/
|
||||
function getGroups(): SearchMatch[][] {
|
||||
function getGroups(matches: SearchMatch[]): SearchMatch[][] {
|
||||
const groups: SearchMatch[][] = []
|
||||
let lastOffset = -1
|
||||
while (true) {
|
||||
@@ -99,6 +94,7 @@ function openSelection(): void {
|
||||
|
||||
<div class="modal-title">Omnisearch - File</div>
|
||||
<CmpInput
|
||||
bind:debouncedValue={searchQuery}
|
||||
on:enter={openSelection}
|
||||
on:arrow-up={() => moveIndex(-1)}
|
||||
on:arrow-down={() => moveIndex(1)}
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { TFile } from "obsidian"
|
||||
import { tick } from "svelte"
|
||||
import { onMount, tick } from "svelte"
|
||||
import CmpInput from "./CmpInput.svelte"
|
||||
import CmpResultNote from "./CmpResultNote.svelte"
|
||||
import type { ResultNote } from "./globals"
|
||||
import { OmnisearchModal } from "./modal"
|
||||
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"
|
||||
|
||||
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
|
||||
scrollIntoView()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await tick()
|
||||
searchQuery = $lastSearch
|
||||
})
|
||||
|
||||
async function createOrOpenNote(item: ResultNote): Promise<void> {
|
||||
try {
|
||||
const file = await $plugin.app.vault.create(
|
||||
$searchQuery + ".md",
|
||||
"# " + $searchQuery
|
||||
searchQuery + ".md",
|
||||
"# " + searchQuery
|
||||
)
|
||||
await $plugin.app.workspace.openLinkText(file.path, "")
|
||||
} catch (e) {
|
||||
@@ -65,7 +78,7 @@ function onInputAltEnter(): void {
|
||||
}
|
||||
|
||||
function moveIndex(dir: 1 | -1): void {
|
||||
selectedIndex = loopIndex(selectedIndex + dir, $resultNotes.length)
|
||||
selectedIndex = loopIndex(selectedIndex + dir, resultNotes.length)
|
||||
scrollIntoView()
|
||||
}
|
||||
|
||||
@@ -83,6 +96,7 @@ function scrollIntoView(): void {
|
||||
|
||||
<div class="modal-title">Omnisearch - Vault</div>
|
||||
<CmpInput
|
||||
bind:debouncedValue={searchQuery}
|
||||
on:enter={onInputEnter}
|
||||
on:shift-enter={onInputShiftEnter}
|
||||
on:ctrl-enter={onInputCtrlEnter}
|
||||
@@ -93,7 +107,7 @@ function scrollIntoView(): void {
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="prompt-results">
|
||||
{#each $resultNotes as result, i}
|
||||
{#each resultNotes as result, i}
|
||||
<CmpResultNote
|
||||
selected={i === selectedIndex}
|
||||
note={result}
|
||||
|
||||
@@ -32,11 +32,13 @@ export const isSearchMatch = (o: { offset?: number }): o is SearchMatch => {
|
||||
}
|
||||
|
||||
export type ResultNote = {
|
||||
// searchResult: SearchResult
|
||||
score: number
|
||||
path: string
|
||||
basename: string
|
||||
content: string
|
||||
foundWords: string[]
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Modal, TFile } from 'obsidian'
|
||||
import type OmnisearchPlugin from './main'
|
||||
import CmpModalVault from './CmpModalVault.svelte'
|
||||
import CmpModalInFile from './CmpModalInFile.svelte'
|
||||
import { inFileSearch, modal } from './stores'
|
||||
import { modal } from './stores'
|
||||
|
||||
export class OmnisearchModal extends Modal {
|
||||
constructor(plugin: OmnisearchPlugin, file?: TFile, canGoBack = false) {
|
||||
@@ -15,7 +15,6 @@ export class OmnisearchModal extends Modal {
|
||||
this.modalEl.append(closeEl)
|
||||
this.modalEl.addClass('omnisearch-modal', 'prompt')
|
||||
|
||||
inFileSearch.set(file ?? null)
|
||||
modal.set(this)
|
||||
|
||||
if (file) {
|
||||
@@ -23,6 +22,7 @@ export class OmnisearchModal extends Modal {
|
||||
target: this.modalEl,
|
||||
props: {
|
||||
canGoBack,
|
||||
singleFilePath: file.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Notice, TFile, type TAbstractFile } from 'obsidian'
|
||||
import MiniSearch, { type SearchResult } from 'minisearch'
|
||||
import type { IndexedNote, ResultNote, SearchMatch } from './globals'
|
||||
import {
|
||||
indexedNotes,
|
||||
inFileSearch as singleFileSearch,
|
||||
plugin,
|
||||
resultNotes,
|
||||
searchQuery,
|
||||
} from './stores'
|
||||
import { SPACE_OR_PUNCTUATION, type IndexedNote, type ResultNote, type SearchMatch } from './globals'
|
||||
import { indexedNotes, plugin } from './stores'
|
||||
import { get } from 'svelte/store'
|
||||
import { extractHeadingsFromCache, stringsToRegex, wait } from './utils'
|
||||
|
||||
@@ -20,6 +14,7 @@ let minisearchInstance: MiniSearch<IndexedNote>
|
||||
export async function initGlobalSearchIndex(): Promise<void> {
|
||||
indexedNotes.set({})
|
||||
minisearchInstance = new MiniSearch({
|
||||
tokenize: text => text.split(SPACE_OR_PUNCTUATION),
|
||||
idField: 'path',
|
||||
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
|
||||
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
|
||||
* @param text
|
||||
@@ -123,7 +93,7 @@ export function getMatches(text: string, reg: RegExp): SearchMatch[] {
|
||||
*/
|
||||
export function getSuggestions(
|
||||
query: string,
|
||||
options?: Partial<{ singleFile: TFile | null }>,
|
||||
options?: Partial<{ singleFilePath: string | null }>,
|
||||
): ResultNote[] {
|
||||
// Get the raw results
|
||||
let results = search(query)
|
||||
@@ -131,9 +101,8 @@ export function getSuggestions(
|
||||
|
||||
// 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)
|
||||
if (options?.singleFilePath) {
|
||||
const result = results.find(r => r.id === options.singleFilePath)
|
||||
if (result) results = [result]
|
||||
else results = []
|
||||
}
|
||||
@@ -150,8 +119,8 @@ export function getSuggestions(
|
||||
const words = Object.keys(result.match)
|
||||
const matches = getMatches(note.content, stringsToRegex(words))
|
||||
const resultNote: ResultNote = {
|
||||
score: result.score,
|
||||
foundWords: words,
|
||||
occurence: 0,
|
||||
matches,
|
||||
...note,
|
||||
}
|
||||
|
||||
@@ -30,17 +30,7 @@ function createIndexedNotes() {
|
||||
/**
|
||||
* If this field is set, the search will be limited to the given file
|
||||
*/
|
||||
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[]>([])
|
||||
// export const inFileSearch = writable<TFile | null>(null)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const indexedNotes = createIndexedNotes()
|
||||
|
||||
export const lastSearch = writable('')
|
||||
|
||||
Reference in New Issue
Block a user