diff --git a/src/__mocks__/obsidian.ts b/src/__mocks__/obsidian.ts index 2579a76..7742258 100644 --- a/src/__mocks__/obsidian.ts +++ b/src/__mocks__/obsidian.ts @@ -495,6 +495,13 @@ export const MockNotice = vi.fn( } ); +export class MockMarkdownView { + editor = { + setCursor: vi.fn(), + scrollIntoView: vi.fn(), + }; +} + /** * Helper to create the {@link Node.createEl} function added by obsidian * diff --git a/src/__mocks__/setupObsidianMocks.ts b/src/__mocks__/setupObsidianMocks.ts index da56c6b..2b4f9a3 100644 --- a/src/__mocks__/setupObsidianMocks.ts +++ b/src/__mocks__/setupObsidianMocks.ts @@ -3,6 +3,7 @@ import { afterEach, vi } from "vitest"; import { MockButtonComponent, MockComponent, + MockMarkdownView, MockModal, MockNotice, MockSetting, @@ -34,5 +35,6 @@ vi.mock("obsidian", () => { }), Setting: MockSetting, ButtonComponent: MockButtonComponent, + MarkdownView: MockMarkdownView, }; }); diff --git a/src/components/TimesheetStatusBar/TimesheetStatusBar.ts b/src/components/TimesheetStatusBar/TimesheetStatusBar.ts index 1725a4b..ed04de5 100644 --- a/src/components/TimesheetStatusBar/TimesheetStatusBar.ts +++ b/src/components/TimesheetStatusBar/TimesheetStatusBar.ts @@ -1,4 +1,5 @@ -import { App, Component, MarkdownView } from "obsidian"; +import { type App } from "obsidian"; +import { Component } from "obsidian"; import { TimekeepEntryItemType, @@ -117,18 +118,6 @@ export class TimesheetStatusBar extends Component { } async onOpen(ref: TimekeepRegistryItemRef) { - const leaf = this.app.workspace.getLeaf(); - await leaf.openFile(ref.file); - - const view = leaf.view; - - if (view instanceof MarkdownView && ref.type === TimekeepEntryItemType.MARKDOWN) { - const editor = view.editor; - const line = ref.position.startLine; - - // Focus the line we opened to - editor.setCursor({ line: Math.max(line - 1, 0), ch: 0 }); - editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true); - } + await TimekeepRegistry.openItemRef(this.app.workspace, ref); } } diff --git a/src/modals/TimekeepLocatorModal.ts b/src/modals/TimekeepLocatorModal.ts index b08618c..3bf1048 100644 --- a/src/modals/TimekeepLocatorModal.ts +++ b/src/modals/TimekeepLocatorModal.ts @@ -1,18 +1,22 @@ -import type { App, TFile } from "obsidian"; +import type { App } from "obsidian"; import { SuggestModal } from "obsidian"; import type { TimekeepSettings } from "@/settings"; import type { Store } from "@/store"; -import type { Timekeep, TimeEntry } from "@/timekeep/schema"; +import type { TimeEntry } from "@/timekeep/schema"; -import { TimekeepEntryItemType, TimekeepRegistry, TimekeepRegistryEntry } from "@/service/registry"; +import { + TimekeepEntryItemType, + TimekeepRegistry, + TimekeepRegistryEntry, + TimekeepRegistryItemRef, +} from "@/service/registry"; import { getRunningEntry } from "@/timekeep/queries"; interface TimekeepResult { - timekeep: Timekeep; + ref: TimekeepRegistryItemRef; running: TimeEntry; - file: TFile; } export class TimekeepLocatorModal extends SuggestModal { @@ -49,18 +53,18 @@ export class TimekeepLocatorModal extends SuggestModal { return this.results.filter((result) => { return ( result.running.name.toLowerCase().contains(queryLower) || - result.file.path.toLowerCase().contains(queryLower) + result.ref.file.path.toLowerCase().contains(queryLower) ); }); } renderSuggestion(value: TimekeepResult, el: HTMLElement) { el.createEl("div", { text: value.running.name }); - el.createEl("small", { text: value.file.path }); + el.createEl("small", { text: value.ref.file.path }); } - onChooseSuggestion(item: TimekeepResult, _evt: MouseEvent | KeyboardEvent) { - void this.app.workspace.getLeaf().openFile(item.file); + async onChooseSuggestion(item: TimekeepResult, _evt: MouseEvent | KeyboardEvent) { + await TimekeepRegistry.openItemRef(this.app.workspace, item.ref); } static getResultsFromEntries(entries: TimekeepRegistryEntry[]): TimekeepResult[] { @@ -71,7 +75,13 @@ export class TimekeepLocatorModal extends SuggestModal { const timekeep = entry.timekeep; const running = getRunningEntry(timekeep.entries); if (running !== null) { - results.push({ file: entry.file, running: running, timekeep }); + results.push({ + running: running, + ref: { + type: entry.type, + file: entry.file, + }, + }); } break; } @@ -80,7 +90,14 @@ export class TimekeepLocatorModal extends SuggestModal { const timekeep = timekeepWithPosition.timekeep; const running = getRunningEntry(timekeep.entries); if (running !== null) { - results.push({ file: entry.file, running: running, timekeep }); + results.push({ + running: running, + ref: { + type: entry.type, + file: entry.file, + position: timekeepWithPosition, + }, + }); } } diff --git a/src/service/registry.test.ts b/src/service/registry.test.ts index 2167c0b..27a3d8d 100644 --- a/src/service/registry.test.ts +++ b/src/service/registry.test.ts @@ -1,14 +1,21 @@ +import type { Workspace } from "obsidian"; + import moment from "moment"; import { v4 } from "uuid"; import { describe, vi, it, expect } from "vitest"; -import { MockVault } from "@/__mocks__/obsidian"; +import { MockMarkdownView, MockVault } from "@/__mocks__/obsidian"; import { defaultSettings } from "@/settings"; import { createStore } from "@/store"; import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema"; import { createCodeBlock } from "@/utils/codeblock"; -import { TimekeepEntryItemType, TimekeepRegistry, TimekeepRegistryEntryMarkdown } from "./registry"; +import { + TimekeepEntryItemType, + TimekeepRegistry, + TimekeepRegistryEntryMarkdown, + TimekeepRegistryItemRef, +} from "./registry"; describe("TimekeepRegistry", () => { describe("getFileRegistryEntry", () => { @@ -641,4 +648,63 @@ describe("TimekeepRegistry", () => { expect(spy).toHaveBeenCalled(); }); }); + + describe("openItemRef", () => { + it("should attempt to open the file", async () => { + const vault = new MockVault(); + const testFile = vault.addFile("test.md", ""); + const ref: TimekeepRegistryItemRef = { + file: testFile, + type: TimekeepEntryItemType.FILE, + }; + + const openFile = vi.fn().mockResolvedValue(undefined); + + const workspace = { + getLeaf() { + const view = new MockMarkdownView(); + (view as any).openFile = openFile; + return view; + }, + } as any as Workspace; + + await TimekeepRegistry.openItemRef(workspace, ref); + + expect(openFile).toHaveBeenCalled(); + }); + + it("opening a markdown entry should scroll to the specific timekeep position", async () => { + const vault = new MockVault(); + const testFile = vault.addFile("test.md", ""); + const ref: TimekeepRegistryItemRef = { + file: testFile, + type: TimekeepEntryItemType.MARKDOWN, + position: { startLine: 1, endLine: 2 }, + }; + + const openFile = vi.fn().mockResolvedValue(undefined); + + const view = new MockMarkdownView(); + + const workspace = { + getLeaf() { + return { view, openFile }; + }, + } as any as Workspace; + + await TimekeepRegistry.openItemRef(workspace, ref); + expect(openFile).toHaveBeenCalled(); + + const line = ref.position.startLine; + + expect(view.editor.setCursor).toHaveBeenCalledExactlyOnceWith({ + line: line - 1, + ch: 0, + }); + expect(view.editor.scrollIntoView).toHaveBeenCalledExactlyOnceWith( + { from: { line, ch: 0 }, to: { line, ch: 0 } }, + true + ); + }); + }); }); diff --git a/src/service/registry.ts b/src/service/registry.ts index 4284909..79102bd 100644 --- a/src/service/registry.ts +++ b/src/service/registry.ts @@ -1,7 +1,7 @@ -import type { TAbstractFile, Vault } from "obsidian"; +import type { TAbstractFile, Vault, Workspace } from "obsidian"; import moment from "moment"; -import { Component, TFile } from "obsidian"; +import { Component, MarkdownView, TFile } from "obsidian"; import { limitFunction } from "p-limit"; import type { TimekeepSettings } from "@/settings"; @@ -357,4 +357,20 @@ export class TimekeepRegistry extends Component { return null; } + + static async openItemRef(workspace: Workspace, ref: TimekeepRegistryItemRef) { + const leaf = workspace.getLeaf(); + await leaf.openFile(ref.file); + + const view = leaf.view; + + if (view instanceof MarkdownView && ref.type === TimekeepEntryItemType.MARKDOWN) { + const editor = view.editor; + const line = ref.position.startLine; + + // Focus the line we opened to + editor.setCursor({ line: Math.max(line - 1, 0), ch: 0 }); + editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true); + } + } }