Skip to content

Commit

Permalink
Support native per-tab history on Obsidian 0.16.3
Browse files Browse the repository at this point in the history
Obsidian 0.16.3 implements a new per-tab history feature similar to Pane Relief's,
so this update integrates support for that feature such that Pane Relief's
enhanced history features still work, i.e.:

- History saved across restarts and workspace loads
- Enhanced history menus with hover preview, drag, and context menu operations
- Forward/back mouse buttons apply to the clicked tab/pane, even if inactive
- Optional page counts on history arrows
- Tooltips on the arrows show the page that will be navigated to

(That is, these features aren't in Obsidian core (yet?) so Pane Relief adds them
on top of the new native history.)

You don't need this update if you aren't using Obsidian 0.16.3 yet, but it
would be a good idea to install it *before* you relaunch for the Obsidian 0.16.3
upgrade.
  • Loading branch information
pjeby committed Sep 19, 2022
1 parent 0aa7b3e commit 8dc0575
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 46 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The overall goal of these features is to provide a more browser-like Obsidian ex

### Per-Tab Navigation History

(Note: Obsidian 0.16.3 implements its own per-tab navigation history, and Pane Relief enhances it to match the feature set described below. Older Obsidian versions have a single global history, and Paen Relief replaces that history with its own implementation that provides these features. This documentation section will be revised accordingly when Pane Relief drops support for older Obsidian versions.)

Normally, Obsidian keeps a single global history for back/forward navigation commands. This history includes not just how you navigate within each tab, but also your navigation *between* tabs. (Which produces counterintuitive results at times, especially if you've pinned any tabs in place, causing *new*, additional panes to be split off when you go "back" or "forward"!)

Pane Relief fixes these problems by giving each tab its own unique back/forward history, just like the tabs in a browser. Going back or forward affects *only* that tab, and no other. If a tab is pinned, a notice is displayed telling you to unpin if you want to go forward or back, instead of opening a new tab. (Messages are also displayed if you try to go further "back" or "forward" than existing history for the tab.)
Expand All @@ -32,6 +34,7 @@ The pages shown in the list can be:
- Dragged from the menu and dropped elsewhere to create a link or move the file
- Clicked to navigate to that position in history (without losing your place),
- Ctrl/Cmd clicked to open a new tab *with duplicated history* at that point in the navigation (similar to doing the same thing in Chrome or Firefox)
- On Obsidian 0.16.3+, you can also use standard Obsidian modifier keys to open the new tab in a new pane or window)
- Right-clicked to open a file context menu to perform actions directly on the file

Last, but far from least, Pane Relief saves each tab's history not only across Obsidian restarts, but *also* saves and loads the history along with workspace layouts, so if you're using the Obsidian workspaces plugin, your navigation history will not get confused by switching between workspaces. (And it even works with Obsidian 0.15.3+'s multiple desktop windows feature.)
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "pane-relief",
"name": "Pane Relief",
"version": "0.3.5",
"version": "0.4.0",
"minAppVersion": "0.15.9",
"description": "Per-tab history, hotkeys for pane/tab movement, navigation, sliding workspace, and more",
"author": "PJ Eby",
Expand Down
24 changes: 21 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 105 additions & 7 deletions src/History.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {Notice, TAbstractFile, ViewState, WorkspaceLeaf} from 'obsidian';
import {HistoryState, Notice, requireApiVersion, TAbstractFile, WorkspaceLeaf} from 'obsidian';
import {around} from "monkey-around";
import {LayoutStorage, Service, windowEvent} from "@ophidian/core";
import { leafName } from './pane-relief';
import { formatState } from './Navigator';

const HIST_ATTR = "pane-relief:history-v1";
const SERIAL_PROP = "pane-relief:history-v1";
export const hasTabHistory = requireApiVersion("0.16.3");

declare module "obsidian" {
interface Workspace {
Expand All @@ -13,11 +15,29 @@ declare module "obsidian" {

interface WorkspaceLeaf {
[HIST_ATTR]: History
history?: LeafHistory
pinned: boolean
working: boolean
serialize(): any
}

interface SerializedHistory {
backHistory: HistoryState[]
forwardHistory: HistoryState[]
}

interface LeafHistory extends SerializedHistory {
go(offset: number): Promise<void>;
deserialize(state: SerializedHistory): void;
}

interface HistoryState {
title: string,
icon: string,
state: ViewState
eState: any
}

interface ViewState {
popstate?: boolean
}
Expand All @@ -29,6 +49,8 @@ export const domLeaves = new WeakMap();
interface PushState {
state: string
eState: string
title: string
icon: string
}

export class HistoryEntry {
Expand All @@ -41,6 +63,22 @@ export class HistoryEntry {
this.setState(rawState);
}

static fromNative(state: HistoryState) {
return new this({...state,
state: JSON.stringify(state.state),
eState: JSON.stringify(state.eState),
});
}

get asNative() {
const state = {...this.raw, state: this.viewState, eState: this.eState};
if (!state.title || !state.icon) {
const info = formatState(this);
state.title ||= (info.title || "");
state.icon ||= (info.icon || "");
}
return state;
}

get viewState() {
return JSON.parse(this.raw.state || "{}")
Expand Down Expand Up @@ -106,22 +144,59 @@ export class History {

static forLeaf(leaf: WorkspaceLeaf) {
if (leaf) domLeaves.set(leaf.containerEl, leaf);
if (leaf) return leaf[HIST_ATTR] instanceof this ?
leaf[HIST_ATTR] :
leaf[HIST_ATTR] = new this(leaf, (leaf[HIST_ATTR]as any)?.serialize() || undefined);
if (leaf) {
const old = leaf[HIST_ATTR] as any;
// Already cached? Return it
if (old instanceof this) return old;
if (old && !old.hadTabs) {
// Try to re-use previous plugin version's state if it wasn't on 0.16.3+
// This will let people who upgrade to 0.16.3 keep their previous history
// if they don't update Pane Relief ahead of time
const oldState: SerializableHistory = old?.serialize() || undefined;
return new this(leaf, oldState).saveToNative();
}
// Either a new install or an update/reload on 0.16.3, get the current state
return new this(leaf).loadFromNative();
}
}

pos: number
stack: HistoryEntry[]
hadTabs = hasTabHistory;

constructor(public leaf?: WorkspaceLeaf, {pos, stack}: SerializableHistory = {pos:0, stack:[]}) {
if (leaf) leaf[HIST_ATTR] = this; // prevent recursive lookups
this.leaf = leaf;
this.pos = pos;
this.stack = stack.map(raw => new HistoryEntry(raw));
}

saveToNative(): this {
const nativeHistory = this.leaf?.history;
if (!nativeHistory || !hasTabHistory) return this;
const stack = this.stack.map(entry => entry.asNative);
nativeHistory.deserialize({
backHistory: stack.slice(this.pos+1).reverse(),
forwardHistory: stack.slice(0, this.pos),
})
return this;
}

loadFromNative(): this {
const history = this.leaf?.history;
if (!history || !hasTabHistory) return this;
const stack: typeof history.backHistory = [].concat(
history.forwardHistory.slice().filter(s => s),
{state: {}, eState: {}},
history.backHistory.slice().filter(s => s).reverse()
);
this.stack = stack.map(e => HistoryEntry.fromNative(e));
this.pos = history.forwardHistory.length;
return this;
}

cloneTo(leaf: WorkspaceLeaf) {
return leaf[HIST_ATTR] = new History(leaf, this.serialize());
return new History(leaf, this.serialize()).saveToNative();
}

onRename(file: TAbstractFile, oldPath: string) {
Expand Down Expand Up @@ -157,7 +232,10 @@ export class History {
// prevent wraparound
const newPos = Math.max(0, Math.min(this.pos - by, this.stack.length - 1));
if (force || newPos !== this.pos) {
this.goto(newPos);
if (this.leaf.history && hasTabHistory) {
this.pos = newPos;
this.leaf.history.go(by);
} else this.goto(newPos);
} else {
new Notice(`No more ${by < 0 ? "back" : "forward"} history for ${leafName}`);
}
Expand Down Expand Up @@ -198,10 +276,30 @@ export class HistoryManager extends Service {

this.registerEvent(store.onLoadItem((item, state) => {
if (item instanceof WorkspaceLeaf && state[SERIAL_PROP]) {
item[HIST_ATTR] = new History(item, state[SERIAL_PROP]);
new History(item, state[SERIAL_PROP]).saveToNative();
}
}));

if (hasTabHistory) {
// Forward native tab history events to our own implementation
this.register(around(WorkspaceLeaf.prototype, {
trigger(old) { return function trigger(name, ...data) {
if (name === "history-change") {
const history = History.forLeaf(this)
history.loadFromNative();
app.workspace.trigger("pane-relief:update-history", this, history);
}
return old.call(this, name, ...data);
}; }
}));

// Incorporate any prior history state (e.g. on plugin update)
if (app.workspace.layoutReady) app.workspace.iterateAllLeaves(leaf => { History.forLeaf(leaf); })

// Skip most actual history replacement if native history is tab-based
return;
}

// Monkeypatch: check for popstate events (to suppress them)
this.register(around(WorkspaceLeaf.prototype, {
setViewState(old) { return function setViewState(vs, es){
Expand Down
Loading

0 comments on commit 8dc0575

Please sign in to comment.