feat: handle focusing existing leaves

Handles finding and focusing existing leaves if they are present instead
of creating new ones
This commit is contained in:
Jacobtread 2026-05-09 10:27:48 +12:00
parent bc913c8218
commit 734113bce4
No known key found for this signature in database
GPG key ID: AB9B37C42B33D9C6
4 changed files with 120 additions and 20 deletions

View file

@ -523,7 +523,18 @@ export const MockNotice = vi.fn(
}
);
export class MockMarkdownView {
export class MockWorkspaceLeaf {
view: unknown;
openFile = vi.fn().mockResolvedValue(undefined);
}
export class MockFileView {
file: TFile | null = null;
}
export class MockEditableFileView extends MockFileView {}
export class MockMarkdownView extends MockEditableFileView {
editor = {
setCursor: vi.fn(),
scrollIntoView: vi.fn(),

View file

@ -3,6 +3,8 @@ import { afterEach, beforeEach, vi } from "vitest";
import {
MockButtonComponent,
MockComponent,
MockEditableFileView,
MockFileView,
MockMarkdownView,
MockModal,
MockNotice,
@ -43,5 +45,7 @@ vi.mock("obsidian", () => {
ButtonComponent: MockButtonComponent,
MarkdownView: MockMarkdownView,
Platform: MockPlatform,
FileView: MockFileView,
EditableFileView: MockEditableFileView
};
});

View file

@ -4,7 +4,7 @@ import moment from "moment";
import { v4 } from "uuid";
import { describe, vi, it, expect } from "vitest";
import { MockMarkdownView, MockVault } from "@/__mocks__/obsidian";
import { MockMarkdownView, MockVault, MockWorkspaceLeaf } from "@/__mocks__/obsidian";
import { defaultSettings, TimekeepSettings } from "@/settings";
import { createStore } from "@/store";
import { createCodeBlock } from "@/utils/codeblock";
@ -761,19 +761,19 @@ describe("TimekeepRegistry", () => {
type: TimekeepEntryItemType.FILE,
};
const openFile = vi.fn().mockResolvedValue(undefined);
const leaf = new MockWorkspaceLeaf();
leaf.view = new MockMarkdownView();
const workspace = {
getLeaf() {
const view = new MockMarkdownView();
(view as any).openFile = openFile;
return view;
return leaf;
},
getLeavesOfType: vi.fn(() => []),
} as any as Workspace;
await TimekeepRegistry.openItemRef(workspace, ref);
expect(openFile).toHaveBeenCalled();
expect(leaf.openFile).toHaveBeenCalled();
});
it("should attempt to open the file in a new tab", async () => {
@ -784,21 +784,73 @@ describe("TimekeepRegistry", () => {
type: TimekeepEntryItemType.FILE,
};
const openFile = vi.fn().mockResolvedValue(undefined);
const getLeaf = vi.fn(() => {
const leaf = new MockWorkspaceLeaf();
leaf.view = new MockMarkdownView();
const fakeLeaf = () => {
const leaf = new MockWorkspaceLeaf();
const view = new MockMarkdownView();
(view as any).openFile = openFile;
return view;
leaf.view = view;
return leaf;
};
const getLeavesOfType = vi.fn(() => {
return [fakeLeaf(), fakeLeaf(), fakeLeaf(), new MockMarkdownView()];
});
const getLeaf = vi.fn(() => {
return leaf;
});
const workspace = {
getLeaf,
getLeavesOfType,
} as any as Workspace;
await TimekeepRegistry.openItemRef(workspace, ref, true);
expect(getLeaf).toHaveBeenCalledWith("tab");
expect(openFile).toHaveBeenCalled();
expect(leaf.openFile).toHaveBeenCalled();
});
it("should focus the existing file instead of opening a new tab if one is present", async () => {
const vault = new MockVault();
const testFile = vault.addFile("test.timekeep", "");
const ref: TimekeepRegistryItemRef = {
file: testFile,
type: TimekeepEntryItemType.FILE,
};
const existingLeaf = new MockWorkspaceLeaf();
const view = new MockMarkdownView();
view.file = testFile;
existingLeaf.view = view;
const revealLeaf = vi.fn().mockResolvedValue(undefined);
const setActiveLeaf = vi.fn().mockResolvedValue(undefined);
const getLeaf = vi.fn(() => {
const existingLeaf = new MockWorkspaceLeaf();
const view = new MockMarkdownView();
existingLeaf.view = view;
return existingLeaf;
});
const getLeavesOfType = vi.fn(() => {
return [existingLeaf];
});
const workspace = {
getLeaf,
getLeavesOfType,
revealLeaf,
setActiveLeaf,
} as any as Workspace;
await TimekeepRegistry.openItemRef(workspace, ref, true);
expect(getLeavesOfType).toHaveBeenCalledWith("timekeep");
expect(getLeaf).not.toHaveBeenCalled();
expect(existingLeaf.openFile).not.toHaveBeenCalled();
expect(revealLeaf).toHaveBeenCalledWith(existingLeaf);
expect(setActiveLeaf).toHaveBeenCalledWith(existingLeaf, { focus: true });
});
it("opening a markdown entry should scroll to the specific timekeep position", async () => {
@ -810,18 +862,19 @@ describe("TimekeepRegistry", () => {
position: { startLine: 1, endLine: 2 },
};
const openFile = vi.fn().mockResolvedValue(undefined);
const leaf = new MockWorkspaceLeaf();
const view = new MockMarkdownView();
leaf.view = view;
const workspace = {
getLeaf() {
return { view, openFile };
return leaf;
},
getLeavesOfType: vi.fn(() => []),
} as any as Workspace;
await TimekeepRegistry.openItemRef(workspace, ref);
expect(openFile).toHaveBeenCalled();
expect(leaf.openFile).toHaveBeenCalled();
const line = ref.position.startLine;

View file

@ -1,7 +1,7 @@
import type { EventRef, TAbstractFile, Vault, Workspace } from "obsidian";
import type { EventRef, TAbstractFile, Vault, Workspace, WorkspaceLeaf } from "obsidian";
import moment from "moment";
import { Component, MarkdownView, TFile } from "obsidian";
import { EditableFileView, Component, MarkdownView, TFile } from "obsidian";
import { limitFunction } from "p-limit";
import type { TimekeepSettings } from "@/settings";
@ -380,6 +380,28 @@ export class TimekeepRegistry extends Component {
return null;
}
/**
* Checks the workspace leaves to see if the provided timekeep item
* ref is already open in a file
*
* @param workspace The workspace to check within
* @param ref The reference to the timekeep
* @returns The existing leaf if found
*/
private static getExistingRefLeaf(workspace: Workspace, ref: TimekeepRegistryItemRef) {
const leavesType = ref.type === TimekeepEntryItemType.FILE ? "timekeep" : "markdown";
const leaves = workspace.getLeavesOfType(leavesType);
for (const leaf of leaves) {
const view = leaf.view;
if (view instanceof EditableFileView && view.file && view.file.path === ref.file.path) {
return leaf;
}
}
return null;
}
/**
* Opens a timekeep by reference to the file and the
* line within the file for markdown timekeep items
@ -392,8 +414,18 @@ export class TimekeepRegistry extends Component {
ref: TimekeepRegistryItemRef,
newTab: boolean = false
) {
const leaf = workspace.getLeaf(newTab ? "tab" : false);
await leaf.openFile(ref.file);
let leaf: WorkspaceLeaf | null = TimekeepRegistry.getExistingRefLeaf(workspace, ref);
// Focus and reveal the existing leaf if found
if (leaf !== null) {
workspace.setActiveLeaf(leaf, { focus: true });
await workspace.revealLeaf(leaf);
}
// Open a new leaf for the ref
else {
leaf = workspace.getLeaf(newTab ? "tab" : false);
await leaf.openFile(ref.file);
}
const view = leaf.view;