diff --git a/src/__mocks__/obsidian.ts b/src/__mocks__/obsidian.ts index 01ce910..925324c 100644 --- a/src/__mocks__/obsidian.ts +++ b/src/__mocks__/obsidian.ts @@ -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(), diff --git a/src/__mocks__/setupObsidianMocks.ts b/src/__mocks__/setupObsidianMocks.ts index ddf209c..c991bff 100644 --- a/src/__mocks__/setupObsidianMocks.ts +++ b/src/__mocks__/setupObsidianMocks.ts @@ -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 }; }); diff --git a/src/service/registry.test.ts b/src/service/registry.test.ts index 5d7bdd5..6da8baf 100644 --- a/src/service/registry.test.ts +++ b/src/service/registry.test.ts @@ -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; diff --git a/src/service/registry.ts b/src/service/registry.ts index 3c9a451..3570a53 100644 --- a/src/service/registry.ts +++ b/src/service/registry.ts @@ -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;