## 実装機能
- 検索条件
- [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
```