Updated flow
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
|||||||
Reference in New Issue
Block a user