## 実装機能 - 検索条件 - [x] スペース区切りでAND検索 - [x] 通常は最終参照時間 > 最終更新時間の優先検索 - [x] `/`はじまりでマッチング率重視検索 - 検索された候補 - [x] **単語出現順序に関係なくヒット** - [x] ディレクトリ名含む検索 - [x] alias含む検索 - [x] 最終更新時間でソート - 候補表示 - [x] 上位パスの表示 - [x] エイリアスの表示 - 選択 - [x] `Ctrl + Enter`で新しいLeafに開く - [x] `Ctrl + Click`で新しいLeafに開く ## プラグインの自動リロード [[プラグインのソースコードが変更されたら自動でリビルドとObsidianリロード]]で実現。 ## コマンドの作成 `Fuzzy Search`で登録。あとで変更するかも ## [[Quick switcher]]のようにリストを出す ### FuzzySuggestModal - [[Quick switcher]]で出てくるあのモーダルダイアログの抽象クラス - 任意型のインスタンスリストを指定できる - テキスト表示内容も指定できる - リストからアイテムが選択されたときの挙動を指定できる ```ts:例 class FuzzySearchModal extends FuzzySuggestModal<TFile> { getItemText(item: TFile): string { return item.name; } getItems(): TFile[] { return this.app.vault.getMarkdownFiles(); } onChooseItem(item: TFile, evt: MouseEvent | KeyboardEvent): void { new Notice(item.name); } } ``` 更に以下3つのメソッドをオーバーライドできそう。 ```ts abstract getSuggestions(query: string): T[]; /** * @public */ abstract renderSuggestion(value: T, el: HTMLElement): any; /** * @public */ abstract onChooseSuggestion(item: T, evt: MouseEvent | KeyboardEvent): any; ``` ## サジェストを最近開いた順にする - `Workspace.getLastOpenFiles(): string[]`が使えそう。 - 直近の10ファイルしか表示されない - **すべてのファイルが対象でないなら検索には使えない** `Modal.onOpen()`より`FuzzySuggestModal.getItems()`の方が先のようだ。 ==これは無理そう== ## サジェストを最近更新した順にする `FuzzySuggestModal.getSuggestions()`を実装してみる。 ```json { "item": TFile, "match": { "matches": [ [0, 6], [9, 11], ], "score": -1.0424 } } ``` 色々頑張ったけど微妙.. fuzzyの呪縛 ```ts getSuggestions(query: string): FuzzyMatch<TFile>[] { const r = this.getItems() .map((x) => ({ item: x, match: fuzzySearch(prepareQuery(query), x.basename), })) .filter((x) => x.match) .filter((x) => x.match.score > -0.1) .sort(sorter((x) => x.match.score, "desc")); console.log(r); return r; } ``` `matches`は範囲が重複すると候補がバグるらしい.. ```ts getSuggestions(query: string): FuzzyMatch<TFile>[] { const qs = query.split(" ").filter((x) => x); const r = this.getItems() .map((x) => ({ item: x, matches: qs .map((q) => getAppearanceRange(x.name, q)) .filter((x) => x != null), })) .filter((x) => x.matches.length === qs.length) .map((x) => ({ item: x.item, match: { score: 1, matches: x.matches, }, })); console.log(r); return r; } ``` ```ts export const getAppearanceRange = ( text: string, query: string ): [begin: number, end: number] | null => { const begin = text.indexOf(query); if (begin === -1) { return null; } return [begin, begin + query.length]; }; ``` `matches`は使わなくていいかも。スコアさえあれば。 ```ts class FuzzySearchModal extends FuzzySuggestModal<TFile> { getSuggestions(query: string): FuzzyMatch<TFile>[] { const qs = query.split(" ").filter((x) => x); return this.getItems() .filter((x) => matchAll(x.path, qs)) .map((x) => ({ item: x, match: { score: x.stat.mtime, matches: [], }, })); } getItemText(item: TFile): string { return `${item.name}`; } getItems(): TFile[] { return this.app.vault.getMarkdownFiles(); } onChooseItem(item: TFile, evt: MouseEvent | KeyboardEvent): void { new Notice(item.name); } } ``` これでクエリによる絞り込みはできた。 ## エイリアスでも検索できるようにする エイリアスを取得するには`CachedMetadata`に保存された`frontmatter`が必要。`parseFrontMatterAliases(frontmatter)`を呼べば`string[]`を得られる。 ```ts interface SuggestionItem { file: TFile; cache?: CachedMetadata; } function lowerInclude(text: string, query: string): boolean { return text.toLowerCase().includes(query.toLowerCase()); } function matchAll(item: SuggestionItem, queries: string[]): boolean { return queries.every( (q) => lowerInclude(item.file.path, q) || (parseFrontMatterAliases(item.cache.frontmatter) ?? []).some((al) => lowerInclude(al, q) ) ); } class FuzzySearchModal extends FuzzySuggestModal<SuggestionItem> { getSuggestions(query: string): FuzzyMatch<SuggestionItem>[] { const qs = query.split(" ").filter((x) => x); return this.getItems() .filter((x) => matchAll(x, qs)) .map((x) => ({ item: x, match: { score: x.file.stat.mtime, matches: [], }, })) .sort(sorter((x) => x.item.file.stat.mtime, "desc")); } getItemText(item: SuggestionItem): string { return `${item.file.path}`; } getItems(): SuggestionItem[] { return this.app.vault.getMarkdownFiles().map((x) => ({ file: x, cache: this.app.metadataCache.getFileCache(x), })); } onChooseItem(item: SuggestionItem, evt: MouseEvent | KeyboardEvent): void { // For Ctrl + Click, not Ctrl + Enter (TODO...) const leaf = this.app.workspace.getLeaf(evt.ctrlKey); leaf.openFile(item.file).then(() => { this.app.workspace.setActiveLeaf(leaf, true, evt.ctrlKey); }); } } ``` ## 候補の表示を変更する アイコン ``` npm install --save @fortawesome/fontawesome-free ``` --- `Ctrl` ``` export type KeymapEventListener = (evt: KeyboardEvent, ctx: KeymapContext) => boolean | void; ``` どうしても選択中の`SuggestionItem`が取り出せなかったので禁断の秘技で.. ```ts export class SmartSearchModal extends FuzzySuggestModal<SuggestionItem> { constructor(app: App) { super(app); this.scope.register( ["Ctrl"], "Enter", (this.scope as any).keys.find( (x: any) => !x.modifiers && x.key === "Enter" ).func ); } ``` ## 手順変更とか ``` npm install ```