> [!warning] > この実装は危険なので推奨しない。以下のプルリクの実装を参考にした方がいい。 > https://github.com/tadashi-aikawa/obsidian-another-quick-switcher/pull/172/files ```ts // これは現在のファイル(履歴情報なので詳細がある)を表示する app.workspace.activeLeaf.getHistoryState() // これは履歴情報が詰まっている app.workspace.activeLeaf.history // 一度退避させて tmpHistory = workspace.activeLeaf.history.backHistory.slice() // あとで代入すると反映できそう workspace.activeLeaf.history.backHistory = tmpHistory ``` ## 方針 - ダイアログを開いたときに `history.backHistory`と`history.forwardHistory`をメンバに保存 - ダイアログを閉じるときに履歴を元に戻す - これだと上記処理の直前に追加された、開くべきファイルの履歴が消えてしまう - onCloseの処理で... - 履歴の最新を取得 - 決定したときだけ考慮できないか... - もしくはファイル開くときに履歴につまないか... - 決定されたかの判定がないと、元に戻すべきかも判定できないのでは... - 決定判定欲しいな... - ==`chooseCurrentSuggestion`は必ず通る..!!== - 基本的には - 選択されなければ - 履歴をもとに戻す - カレントファイルを戻した履歴の先頭にする ## 問題 ### 最後のプレビューが履歴に残る - 手順 1. `Untitled 1`を開く 2. `Untitled 2`を開く 3. `hoge1`をプレビュー 4. `hoge2`をプレビュー 5. `Untitled 3`を開く - 結果 - 履歴が以下の順になっている - `hoge2` <== 期待値はこれがないこと - `Untitled2` - `Untitled 1` - 備考 - 同一Leafを開いた場合のみ (そのほかは問題ない) - `ESC`でキャンセルだと期待通りになる - デフォルトのkey bind handlerでも自作でも起こる - 原因 - `onClose`でhistoryを復元したあとに、直前の履歴がhistoryに追加されていそう - これはファイルを開いたときに積み立てられるから難しいなあ... - プレビューじゃない場合はこれが期待通りなので... ### Enterやマウス左クリックなどで選択したらファイルが開かない #### 原因 `onChooseSuggestion`より`onClose`の方が早く呼び出されるから。`onClose`には以下の処理がある。 ```ts if (!this.willOpenInSameLeaf) { this.appHelper.resetCurrentLeafHistoryStateTo(leaf, this.initialHistory); this.appHelper.setLeafForwardHistories(leaf, this.forwardHistories); } ``` `this.willOpenInSameLeaf`は`onChooseSuggestion`にて、同じリーフにファイルを開く場合のみ`true`に変更されるオプション。 ```ts if (!option.keepOpen) { if (leaf === "same-tab") { this.willOpenInSameLeaf = true; } this.close(); } ``` 本来なら今回のケースでは`true`に変更される...が、`onChooseSuggestion`の前に`onClose`が呼び出されるのでどうしようもない。`false`のままで突っ込んでしまう。 `this.willOpenInSameLeaf`が`false`の場合、ダイアログを開く直前にリーフで開かれていたファイルがアクティブになり、そこから先のforward historiesがすべて復元される。つまり、ダイアログを開く直前のファイル(カーソル位置含む)と履歴が反映される。 このようなケースの具体例は以下の2パターンがある。 - キャンセルして閉じたとき - 新しいタブや新しいウィンドウなど、現在のリーフ以外でファイルを開いたとき どちらもリーフはダイアログを開く直前の状態であることが期待値となる。 ん...? 結局、どのようなオペレーションをしても、履歴を復元することに変わりはなさそう。つまり、`this.willOpenInSameLeaf`は必要ないのでは? いや、戻してしまうと、そのあとに新しいファイルがスムーズに開かない問題があった気がする。とはいえ、今はその問題があったケースで新たな問題が出ているわけだ。ここも含めて考え直した方がいいかも。。 あ、最後に開いているファイルが履歴に存在しない問題の対応かな...。 #### `this.willOpenInSameLeaf`が不要ならどうなる? `onClose`の処理は気にしなくてOKになる。いずれの場合もダイアログを開く直前の履歴を復元。 この処理は? ```ts setTimeout(() => { const previewedPaths = this.previewedFiles.map((x) => x.path); const { backHistories } = this.appHelper.cloneLeafHistories(leaf); this.appHelper.setLeafBackHistories( leaf, backHistories.filter( (x) => !previewedPaths.includes(x.state.state.file) ) ); }, 200); ``` backHistoriesからpreviewしたファイルを除外するお仕事。`this.willOpenInSameLeaf`が`false`の場合はこれでケアしていたというわけか...なくても動いてそうに見えるが... 問題はやはり、選択したファイルが開かないこと。 #### 詳細調査 ファイルをEnterで決定したときの詳細。 1. `onClose` 2. `chooseCurrentSuggestion` 3. `onClose` `chooseCurrentSuggestion`にファイルは渡っているので、一瞬だけでも選択したファイルは開いているはず。その後の`close`で問題が起こってそう。`close`を外してみる。 ```ts if (!option.keepOpen) { if (leaf === "same-tab") { this.willOpenInSameLeaf = true; } //this.close(); } ``` `this.close()`をコメントアウトしても変化がなかった。`onClose`のあとに`chooseCurrentSuggestion`が呼び出されることによって、予期せぬパラメータ変更が働いている?? ```ts openFile(file: TFile, option: Partial<OpenFileOption> = {}) { console.log("openFile"); console.log(file); console.log(option); ``` ここのログは完全に期待通りである。ではなぜ... Leafの`history.updateState`を見てみる。 ```js return e.prototype.updateState = function(e) { return g(this, void 0, void 0, (function() { var t, n; return v(this, (function(i) { switch (i.label) { case 0: return t = e.state, n = e.eState, t.popstate = !0, t.active = Mw.isDesktop, [4, this.owner.setViewState(t, n)]; case 1: return i.sent(), this.owner.trigger("history-change"), [2] } } )) } )) } ``` あれ... これはいわゆる非同期関数では..。インターフェース定義は`unknown`でごまかしてしまってたけどもしや... ```ts interface UnSafeWorkspaceLeaf extends WorkspaceLeaf { history: { backHistory: UnsafeHistory[]; forwardHistory: UnsafeHistory[]; updateState(history: UnsafeHistory): unknown; }; getHistoryState(): UnsafeHistory; } ``` とりあえずここは[[Promise (JavaScript)|Promise]]だった。 ```ts updateState(history: UnsafeHistory): Promise<unknown>; ``` だがこれでもダメ。ログを出してみると ```ts async onClose() { console.log("onClose"); super.onClose(); if (this.willSilentClose) { return; } const leaf = this.app.workspace.getLeaf(); await this.appHelper.resetCurrentLeafHistoryStateTo( leaf, this.initialHistory ); this.appHelper.setLeafForwardHistories(leaf, this.forwardHistories); console.log("onClose end"); } ``` 見事に`onClose`が`async`関数として機能していない。。。 ``` onClose chooseCurrentSuggestion onClose onClose end onClose end ``` superクラスの`onClose`はこうだから仕方ない。。なるほど。。 ```ts onClose(): void; ``` #### やりたいこと - ダイアログを閉じる直前にリーフの履歴/状態を復元したい - 復元後、必要があれば新しいファイルを開きたい ...ということはファイルオープンに `setTimeout` かませるしかないのでは。。 - `onClose`で`this.isOpen`を見る - `true` だと - カスタムハンドラー - `ESC` - ダイアログの外をクリック - `false` だと - `Enter` などのデフォルトキーバインド - 実際やってみたら `true` だったんだが。。どうして - `this.isOpen`が`false`のとき = Enterとかクリックのとき - `chooseCurrentSuggestion`が終わる前の`onClose`は無効化したい #### 残された手は。。。 イベントハンドラを再定義しなおすしかない気がする。。 ```ts , gR = function() { function e(e, t, n) { var i = this; this.chooser = e, this.containerEl = t, this.values = [], this.suggestions = [], this.selectedItem = 0, t.on("click", ".suggestion-item", this.onSuggestionClick.bind(this)), t.on("auxclick", ".suggestion-item", this.onSuggestionClick.bind(this)), t.on("mousemove", ".suggestion-item", this.onSuggestionMouseover.bind(this)); ``` これだと既存のハンドラが削除されなくてNG。 #### スレッド管理のように状態管理する `onClose`の履歴リセット処理に対して状態を持たせる。 ```ts const leaf = this.app.workspace.getLeaf(); if (!this.openInSameLeaf) { this.historyRestoreStatus = "doing"; this.appHelper .resetCurrentLeafHistoryStateTo(leaf, this.initialHistory) .then(() => { this.appHelper.setLeafForwardHistories(leaf, this.forwardHistories); this.historyRestoreStatus = "done"; }); } ``` 履歴リセット中の場合は、リセットが完了するまでファイルを開かずにループで待機する。これによって、履歴を戻した後に本来開くファイルを追加することができる。 間をあけると一瞬元ファイルが見えるので0secでタスクを入れ替える感じ。 ```ts async chooseCurrentSuggestion( leaf: LeafType, option: { keepOpen?: boolean } = {} ): Promise<TFile | null> { const item = this.chooser.values?.[this.chooser.selectedItem]; if (!item) { return null; } if (!option.keepOpen) { if (leaf === "same-tab") { this.openInSameLeaf = true; } this.close(); } // HACK: For click after preview handling if (this.historyRestoreStatus === "doing") { // @ts-ignore while (this.historyRestoreStatus !== "done") { await sleep(0); } } this.appHelper.openFile(item.file, { leaf: leaf, line: item.lineNumber - 1, }); return item.file; } ``` このif文に入るのはデフォルトのイベントハンドラによる処理がされるときのみ。すなわち... - マウスの左クリック - タッチ の場合のみのはず...。もともとキーボード操作がメインのプラグインなので、マウスを充実させなくてもいい気がする。ただ、スマホのタッチができないのは致命的なので、そこだけ動くようにしたかった。ベータ版で本リリース前に確認する。