From d4143c71d37502a38253f97ceb6184c3c3d58d41 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 16:09:16 +0530 Subject: [PATCH 01/13] Updated md parser to find blocks --- packages/core/src/index/parser/md.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/index/parser/md.ts b/packages/core/src/index/parser/md.ts index 133579f..f42dec7 100644 --- a/packages/core/src/index/parser/md.ts +++ b/packages/core/src/index/parser/md.ts @@ -51,3 +51,11 @@ export function extractTags(markdown: string): string[] { } return [...seen]; } + +export function findBlockPosition(markdown: string, blockId: string): number | null { + const re = new RegExp(`\\^${blockId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "gm"); + const match = re.exec(markdown); + if (!match) return null; + const lineStart = markdown.lastIndexOf("\n", match.index) + 1; + return lineStart; +} From d144fec5f12eaa252e68f898d4a85c75dc21def8 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 16:12:11 +0530 Subject: [PATCH 02/13] Updated wikilinks to add options for block id --- packages/editor/src/extensions/wikilinks/wikilinks.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/extensions/wikilinks/wikilinks.ts b/packages/editor/src/extensions/wikilinks/wikilinks.ts index eb6cf57..c7735f1 100644 --- a/packages/editor/src/extensions/wikilinks/wikilinks.ts +++ b/packages/editor/src/extensions/wikilinks/wikilinks.ts @@ -6,12 +6,12 @@ import { type EditorView, type ViewUpdate, } from "@codemirror/view"; -import { resolveWikilink } from "../resolver/resolver.js"; +import { parseLinkTarget, resolveWikilink } from "../resolver/resolver.js"; import "./wikilinks.css"; export interface WikilinkOptions { getIndex: () => ReadonlyMap; - onOpen: (path: string) => void; + onOpen: (path: string, blockId?: string) => void; } const linkRe = /\[\[([^\[\]\n]+)\]\]/g; @@ -62,8 +62,10 @@ export function handleWikilinkClick( const from = line.from + match.index; const to = from + match[0].length; if (pos >= from && pos <= to) { - const path = resolveWikilink(match[1]!, opts.getIndex()); - if (path !== null) opts.onOpen(path); + const raw = match[1]!; + const { note, blockId } = parseLinkTarget(raw); + const path = resolveWikilink(note, opts.getIndex()); + if (path !== null) opts.onOpen(path, blockId ?? undefined); return path; } } From fb492cd599ee0d0546a79b176100196e3024769d Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 16:40:59 +0530 Subject: [PATCH 03/13] Feat: Updated CodeMirror for handling links, blocks --- packages/editor/src/CodeMirrorEditor.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/CodeMirrorEditor.tsx b/packages/editor/src/CodeMirrorEditor.tsx index 055e5ff..e5e1981 100644 --- a/packages/editor/src/CodeMirrorEditor.tsx +++ b/packages/editor/src/CodeMirrorEditor.tsx @@ -9,10 +9,11 @@ interface Props { value: string; onChange?: (value: string) => void; links?: ReadonlyMap; - onOpenLink?: (path: string) => void; + onOpenLink?: (path: string, blockId?: string) => void; + scrollToPos?: { from: number; to: number } | null; } -export function CodeMirrorEditor({ value, onChange, links, onOpenLink }: Props) { +export function CodeMirrorEditor({ value, onChange, links, onOpenLink, scrollToPos }: Props) { const editorRef = useRef(null); const viewRef = useRef(null); const initialValueRef = useRef(value); @@ -43,7 +44,7 @@ export function CodeMirrorEditor({ value, onChange, links, onOpenLink }: Props) markdown(), wikilinks({ getIndex: () => linksRef.current, - onOpen: (path) => onOpenLinkRef.current?.(path), + onOpen: (path, blockId) => onOpenLinkRef.current?.(path, blockId), }), blockRefs(), EditorView.updateListener.of((update: ViewUpdate) => { @@ -60,11 +61,23 @@ export function CodeMirrorEditor({ value, onChange, links, onOpenLink }: Props) return () => view.destroy(); }, []); + const scrollToPosRef = useRef(scrollToPos); + useEffect(() => { + scrollToPosRef.current = scrollToPos; + }, [scrollToPos]); + useEffect(() => { const view = viewRef.current; if (view && value !== view.state.doc.toString()) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }); } + // NEW: handle scrollTo after content is set + if (view && scrollToPosRef.current) { + view.dispatch({ + selection: { anchor: scrollToPosRef.current.from, head: scrollToPosRef.current.to }, + scrollIntoView: true, + }); + } }, [value]); return
; From d52a8c20d9b69a8f97559e076e1a2708e186284c Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 16:56:44 +0530 Subject: [PATCH 04/13] Feat: Added a backlinks panel --- .../src/features/editor/BacklinksPane.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 apps/desktop/src/features/editor/BacklinksPane.tsx diff --git a/apps/desktop/src/features/editor/BacklinksPane.tsx b/apps/desktop/src/features/editor/BacklinksPane.tsx new file mode 100644 index 0000000..ba73595 --- /dev/null +++ b/apps/desktop/src/features/editor/BacklinksPane.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import { tauriIndexDriver } from "../../ipc/index-driver"; + +interface Props { + vaultPath: string; + targetRelPath: string; + onOpenNote: (relPath: string) => void; +} + +export default function BacklinksPane({ vaultPath, targetRelPath, onOpenNote }: Props) { + const [backlinks, setBacklinks] = useState<{ sourcePath: string; positionChar: number }[]>([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + async function load() { + setLoading(true); + const results = await tauriIndexDriver.backlinks(vaultPath, targetRelPath); + if (!cancelled) { + setBacklinks(results); + setLoading(false); + } + } + void load(); + return () => { + cancelled = true; + }; + }, [vaultPath, targetRelPath]); + + if (loading) return
Loading backlinks…
; + if (backlinks.length === 0) return
No backlinks
; + + return ( + + ); +} From d173fa5243617d7973228d32be1cf305056a8304 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 17:22:19 +0530 Subject: [PATCH 05/13] Feat: Updated exports --- packages/core/src/index/index.ts | 8 +++++++- packages/core/src/index/parser/index.ts | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/index/parser/index.ts diff --git a/packages/core/src/index/index.ts b/packages/core/src/index/index.ts index 679d807..cf653b3 100644 --- a/packages/core/src/index/index.ts +++ b/packages/core/src/index/index.ts @@ -1,4 +1,10 @@ export { Indexer, type IndexerDeps } from "./indexer.js"; export { MemoryIndexDriver, type IndexDriver } from "./driver.js"; -export { extractIndexContent, extractHeadings, extractLinks, extractTags } from "./parser/md.js"; +export { + extractIndexContent, + extractHeadings, + extractLinks, + extractTags, + findBlockPosition, +} from "./parser/md.js"; export { type IndexedFileMeta, type IndexEvent, type ExtractedNote } from "./types.js"; diff --git a/packages/core/src/index/parser/index.ts b/packages/core/src/index/parser/index.ts new file mode 100644 index 0000000..d801745 --- /dev/null +++ b/packages/core/src/index/parser/index.ts @@ -0,0 +1,7 @@ +export { + extractIndexContent, + extractHeadings, + extractLinks, + extractTags, + findBlockPosition, +} from "./md.js"; From e674e5c38663f6e9b86b17f41c78fc4405f406e3 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 17:22:36 +0530 Subject: [PATCH 06/13] Feat: Updated Home with backlinkpane --- apps/desktop/src/routes/Home.tsx | 74 ++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/routes/Home.tsx b/apps/desktop/src/routes/Home.tsx index b0ec993..c5408c4 100644 --- a/apps/desktop/src/routes/Home.tsx +++ b/apps/desktop/src/routes/Home.tsx @@ -7,6 +7,7 @@ import { fsEventToVaultEvent, joinPath, Indexer, + findBlockPosition } from "@trachyte/core"; import { tauriDriver } from "../ipc/driver"; import { onFsEvent, startWatcher } from "../ipc/watcher"; @@ -15,6 +16,7 @@ import { tauriIndexDriver } from "../ipc/index-driver"; import DoctorPanel from "../features/doctor/DoctorPanel"; import SearchPalette from "../features/search/SearchPalette"; +import BacklinksPane from "../features/editor/BacklinksPane"; export default function Home() { const [inputPath, setInputPath] = useState(""); @@ -28,6 +30,7 @@ export default function Home() { const reloader = useMemo(() => new ExternalChangeReloader(manager.events, 100), [manager]); const [doctorOpen, setDoctorOpen] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); + const [scrollToPos, setScrollToPos] = useState<{ from: number; to: number } | null>(null); const linkIndex = useMemo( () => new Map(notes.map((n) => [n.replace(/\.md$/i, ""), `Notes/${n}`])), @@ -61,11 +64,23 @@ export default function Home() { await startWatcher(path); } - async function openNote(rel: string) { + async function openNote(rel: string, blockId?: string) { if (openedVault === null) return; setCurrentRel(rel); - setContent(await manager.getAdapter().readFile(joinPath(openedVault, rel))); + const text = await manager.getAdapter().readFile(joinPath(openedVault, rel)); + setContent(text); reloader.watch(rel); + + if (blockId) { + const pos = findBlockPosition(text, blockId); + if (pos !== null) { + setScrollToPos({ from: pos, to: pos }); + } else { + setScrollToPos(null); + } + } else { + setScrollToPos(null); + } } useEffect(() => { @@ -108,29 +123,44 @@ export default function Home() { e.preventDefault(); setDoctorOpen((v) => !v); } + if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "f") { + e.preventDefault(); + setPaletteOpen((v) => !v); + } } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); + // useEffect(() => { + // function onKeyDown(e: KeyboardEvent) { + // if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "d") { + // e.preventDefault(); + // setDoctorOpen((v) => !v); + // } + // } + // window.addEventListener("keydown", onKeyDown); + // return () => window.removeEventListener("keydown", onKeyDown); + // }, []); + useEffect(() => { return () => indexer?.dispose(); }, [indexer]); - useEffect(() => { - function onKeyDown(e: KeyboardEvent) { - if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "d") { - e.preventDefault(); - setDoctorOpen((v) => !v); - } - if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "f") { - e.preventDefault(); - setPaletteOpen((v) => !v); - } - } - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, []); + // useEffect(() => { + // function onKeyDown(e: KeyboardEvent) { + // if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "d") { + // e.preventDefault(); + // setDoctorOpen((v) => !v); + // } + // if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "f") { + // e.preventDefault(); + // setPaletteOpen((v) => !v); + // } + // } + // window.addEventListener("keydown", onKeyDown); + // return () => window.removeEventListener("keydown", onKeyDown); + // }, []); return (
@@ -177,7 +207,7 @@ export default function Home() { ))} -
+
{currentRel === null ? (
Open a note to start editing @@ -187,7 +217,15 @@ export default function Home() { value={content} onChange={setContent} links={linkIndex} - onOpenLink={(rel) => void openNote(rel)} + onOpenLink={(path, blockId) => void openNote(path, blockId)} + scrollToPos={scrollToPos} + /> + )} + {currentRel && ( + void openNote(path)} /> )}
From ce0271d94eb0f402911297b353b5a43dab42b755 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 17:26:56 +0530 Subject: [PATCH 07/13] Fix: Updated wikilinks tests to count for blocksIds --- .../src/extensions/wikilinks/__tests__/wikilinks.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts b/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts index fdb6f15..a65e940 100644 --- a/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts +++ b/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts @@ -49,7 +49,7 @@ describe("wikilinks extension", () => { onOpen, }); expect(path).toBe("Notes/Java.md"); - expect(onOpen).toHaveBeenCalledWith("Notes/Java.md"); + expect(onOpen).toHaveBeenCalledWith("Notes/Java.md", undefined); view.destroy(); }); @@ -60,7 +60,7 @@ describe("wikilinks extension", () => { onOpen, }); expect(path).toBe("Notes/Java.md"); - expect(onOpen).toHaveBeenCalledWith("Notes/Java.md"); + expect(onOpen).toHaveBeenCalledWith("Notes/Java.md", undefined); view.destroy(); }); From 9964ed6b8ce80e2ca1c0a09bd8c4ef44fb5e5fde Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 17:36:42 +0530 Subject: [PATCH 08/13] Feat: Updated, added tests --- .../editor/__tests__/BacklinksPane.spec.tsx | 82 +++++++++++++++++++ packages/editor/package.json | 12 +-- .../wikilinks/__tests__/wikilinks.spec.ts | 15 +++- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx diff --git a/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx new file mode 100644 index 0000000..3cd75fa --- /dev/null +++ b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx @@ -0,0 +1,82 @@ +// @vitest-environment jsdom +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import BacklinksPane from "../BacklinksPane"; +import type { IndexDriver } from "@trachyte/core"; + +// Mock the driver with proper typing +const mockBacklinks = vi.fn(); + +vi.mock("../../../ipc/index-driver", () => ({ + tauriIndexDriver: { + backlinks: mockBacklinks, + }, +})); + +// import { tauriIndexDriver } from "../../../ipc/index-driver"; + +describe("BacklinksPane", () => { + const mockOnOpenNote = vi.fn(); + const props = { + vaultPath: "/test/vault", + targetRelPath: "Notes/Java.md", + onOpenNote: mockOnOpenNote, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("shows loading state initially", () => { + mockBacklinks.mockResolvedValue([]); + render(); + expect(screen.getByText("Loading backlinks…")).toBeInTheDocument(); + }); + + it("shows empty state when no backlinks", async () => { + mockBacklinks.mockResolvedValue([]); + render(); + await screen.findByText("No backlinks"); + }); + + it("renders backlinks with source name and position", async () => { + mockBacklinks.mockResolvedValue([ + { sourcePath: "Notes/Foo.md", positionChar: 42 }, + { sourcePath: "Notes/Bar.md", positionChar: 10 }, + ]); + render(); + await screen.findByText("Foo"); + await screen.findByText("char 42"); + await screen.findByText("Bar"); + await screen.findByText("char 10"); + }); + + it("calls onOpenNote when a backlink is clicked", async () => { + mockBacklinks.mockResolvedValue([{ sourcePath: "Notes/Foo.md", positionChar: 42 }]); + render(); + await screen.findByText("Foo"); + fireEvent.click(screen.getByText("Foo")); + expect(mockOnOpenNote).toHaveBeenCalledWith("Notes/Foo.md"); + }); + + it("sorts backlinks by source path then position", async () => { + mockBacklinks.mockResolvedValue([ + { sourcePath: "Notes/B.md", positionChar: 5 }, + { sourcePath: "Notes/A.md", positionChar: 10 }, + { sourcePath: "Notes/A.md", positionChar: 2 }, + ]); + render(); + const items = await screen.findAllByRole("button"); + expect(items[0]).toHaveTextContent("A"); + expect(items[1]).toHaveTextContent("A"); + expect(items[2]).toHaveTextContent("B"); + }); + + it("has accessible structure", () => { + mockBacklinks.mockResolvedValue([]); + render(); + const aside = screen.getByLabelText("Backlinks"); + expect(aside).toBeInTheDocument(); + expect(aside).toHaveAttribute("role", "complementary"); + }); +}); diff --git a/packages/editor/package.json b/packages/editor/package.json index ba18e1f..fec5225 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -10,20 +10,20 @@ "test": "vitest run" }, "dependencies": { - "react": "catalog:", - "codemirror": "^6.0.2", + "@codemirror/commands": "^6.8.2", "@codemirror/lang-markdown": "^6.5.1", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.43.6", - "@codemirror/commands": "^6.8.2", - "@trachyte/core": "workspace:*" + "@trachyte/core": "workspace:*", + "codemirror": "^6.0.2", + "react": "catalog:" }, "peerDependencies": { "react": "^19.0.0" }, "devDependencies": { "@types/react": "catalog:", - "vitest": "^4", - "jsdom": "^29" + "jsdom": "^29", + "vitest": "^4" } } diff --git a/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts b/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts index a65e940..385842a 100644 --- a/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts +++ b/packages/editor/src/extensions/wikilinks/__tests__/wikilinks.spec.ts @@ -42,7 +42,7 @@ describe("wikilinks extension", () => { view.destroy(); }); - it("opens a resolved link on click", () => { + it("opens a resolved link on click (no blockId)", () => { const { view, onOpen } = makeView("[[Java]]", new Map([["Java", "Notes/Java.md"]])); const path = handleWikilinkClick(view, insideLinkPos(view, "Java"), { getIndex: () => new Map([["Java", "Notes/Java.md"]]), @@ -53,7 +53,7 @@ describe("wikilinks extension", () => { view.destroy(); }); - it("opens an escape-hatch path on click", () => { + it("opens an escape-hatch path on click (no blockId)", () => { const { view, onOpen } = makeView("[[Notes/Java.md]]", new Map()); const path = handleWikilinkClick(view, insideLinkPos(view, "Notes/Java.md"), { getIndex: () => new Map(), @@ -64,6 +64,17 @@ describe("wikilinks extension", () => { view.destroy(); }); + it("opens a link with blockId on click", () => { + const { view, onOpen } = makeView("[[Java#^block123]]", new Map([["Java", "Notes/Java.md"]])); + const path = handleWikilinkClick(view, insideLinkPos(view, "Java#^block123"), { + getIndex: () => new Map([["Java", "Notes/Java.md"]]), + onOpen, + }); + expect(path).toBe("Notes/Java.md"); + expect(onOpen).toHaveBeenCalledWith("Notes/Java.md", "block123"); + view.destroy(); + }); + it("does not open an unresolved link", () => { const { view, onOpen } = makeView("[[Nope]]", new Map()); const path = handleWikilinkClick(view, insideLinkPos(view, "Nope"), { From a9d90ece3234ffb6269970a08e142cb2c4f4ad65 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 17:41:52 +0530 Subject: [PATCH 09/13] Fix: Updated BackPane tests --- .../src/features/editor/__tests__/BacklinksPane.spec.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx index 3cd75fa..50936a8 100644 --- a/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx +++ b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx @@ -4,8 +4,10 @@ import { render, screen, fireEvent } from "@testing-library/react"; import BacklinksPane from "../BacklinksPane"; import type { IndexDriver } from "@trachyte/core"; -// Mock the driver with proper typing -const mockBacklinks = vi.fn(); +// Use vi.hoisted to ensure mock is available at hoist time +const { mockBacklinks } = vi.hoisted(() => ({ + mockBacklinks: vi.fn(), +})); vi.mock("../../../ipc/index-driver", () => ({ tauriIndexDriver: { @@ -13,8 +15,6 @@ vi.mock("../../../ipc/index-driver", () => ({ }, })); -// import { tauriIndexDriver } from "../../../ipc/index-driver"; - describe("BacklinksPane", () => { const mockOnOpenNote = vi.fn(); const props = { From d5afe1d9bccfb3cc14aab4e6fadd86a1af4df194 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 18:40:31 +0530 Subject: [PATCH 10/13] Fix: Added sorting seperate from driver so it sorts regardless --- apps/desktop/src/features/editor/BacklinksPane.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/features/editor/BacklinksPane.tsx b/apps/desktop/src/features/editor/BacklinksPane.tsx index ba73595..a2c5122 100644 --- a/apps/desktop/src/features/editor/BacklinksPane.tsx +++ b/apps/desktop/src/features/editor/BacklinksPane.tsx @@ -16,8 +16,11 @@ export default function BacklinksPane({ vaultPath, targetRelPath, onOpenNote }: async function load() { setLoading(true); const results = await tauriIndexDriver.backlinks(vaultPath, targetRelPath); + const sorted = [...results].sort( + (a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.positionChar - b.positionChar, + ); if (!cancelled) { - setBacklinks(results); + setBacklinks(sorted); setLoading(false); } } From 541d228c3d74e3445dc7dfdbc16f5b570af8bb11 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 18:40:46 +0530 Subject: [PATCH 11/13] Fix: Updated tests to match --- .../editor/__tests__/BacklinksPane.spec.tsx | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx index 50936a8..8f5c37b 100644 --- a/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx +++ b/apps/desktop/src/features/editor/__tests__/BacklinksPane.spec.tsx @@ -1,10 +1,9 @@ // @vitest-environment jsdom import { describe, expect, it, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; import BacklinksPane from "../BacklinksPane"; import type { IndexDriver } from "@trachyte/core"; -// Use vi.hoisted to ensure mock is available at hoist time const { mockBacklinks } = vi.hoisted(() => ({ mockBacklinks: vi.fn(), })); @@ -29,13 +28,17 @@ describe("BacklinksPane", () => { it("shows loading state initially", () => { mockBacklinks.mockResolvedValue([]); - render(); + act(() => { + render(); + }); expect(screen.getByText("Loading backlinks…")).toBeInTheDocument(); }); it("shows empty state when no backlinks", async () => { mockBacklinks.mockResolvedValue([]); - render(); + act(() => { + render(); + }); await screen.findByText("No backlinks"); }); @@ -44,7 +47,9 @@ describe("BacklinksPane", () => { { sourcePath: "Notes/Foo.md", positionChar: 42 }, { sourcePath: "Notes/Bar.md", positionChar: 10 }, ]); - render(); + act(() => { + render(); + }); await screen.findByText("Foo"); await screen.findByText("char 42"); await screen.findByText("Bar"); @@ -53,7 +58,9 @@ describe("BacklinksPane", () => { it("calls onOpenNote when a backlink is clicked", async () => { mockBacklinks.mockResolvedValue([{ sourcePath: "Notes/Foo.md", positionChar: 42 }]); - render(); + act(() => { + render(); + }); await screen.findByText("Foo"); fireEvent.click(screen.getByText("Foo")); expect(mockOnOpenNote).toHaveBeenCalledWith("Notes/Foo.md"); @@ -65,18 +72,24 @@ describe("BacklinksPane", () => { { sourcePath: "Notes/A.md", positionChar: 10 }, { sourcePath: "Notes/A.md", positionChar: 2 }, ]); - render(); + act(() => { + render(); + }); + // Direct findAllByRole - waits for buttons to appear const items = await screen.findAllByRole("button"); + // Component sorts: A (pos 2), A (pos 10), B (pos 5) expect(items[0]).toHaveTextContent("A"); expect(items[1]).toHaveTextContent("A"); expect(items[2]).toHaveTextContent("B"); }); - it("has accessible structure", () => { - mockBacklinks.mockResolvedValue([]); - render(); + it("has accessible structure when backlinks exist", async () => { + mockBacklinks.mockResolvedValue([{ sourcePath: "Notes/A.md", positionChar: 1 }]); + act(() => { + render(); + }); + await waitFor(() => screen.getByText("A")); const aside = screen.getByLabelText("Backlinks"); expect(aside).toBeInTheDocument(); - expect(aside).toHaveAttribute("role", "complementary"); }); }); From 9c994c1d9b9bed9d4f41477d2f23643989fa8469 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 18:41:40 +0530 Subject: [PATCH 12/13] Fix: Formatting fixes --- apps/desktop/src/routes/Home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/Home.tsx b/apps/desktop/src/routes/Home.tsx index c5408c4..7325f04 100644 --- a/apps/desktop/src/routes/Home.tsx +++ b/apps/desktop/src/routes/Home.tsx @@ -7,7 +7,7 @@ import { fsEventToVaultEvent, joinPath, Indexer, - findBlockPosition + findBlockPosition, } from "@trachyte/core"; import { tauriDriver } from "../ipc/driver"; import { onFsEvent, startWatcher } from "../ipc/watcher"; From 79187b300ca7248793947d81c5ab439374942582 Mon Sep 17 00:00:00 2001 From: Debankan Roy Date: Tue, 18 Aug 2026 18:57:12 +0530 Subject: [PATCH 13/13] Updated Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab34402..ee92c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Link/heading/tag data layer: MD parser extracts `[[wikilinks]]` (char-offset positions), `#headings`, and `#tags`; `Indexer` persists them to the `headings`/`tags`/`backlinks` tables via the new `IndexDriver.storeExtracted` method; `IndexDriver.backlinks(vaultPath, target)` query returns source notes + char positions (Rust `store_extracted`/`list_backlinks`, IPC `index_store_extracted`/`index_list_backlinks`, desktop wrappers); wikilink resolver relocated into core and shared with the editor +- BacklinksPane: right-hand pane showing backlinks for the open note with click-to-navigate +- Wikilink block references: `[[note#^blockId]]` now scrolls to the `^blockId` line in the target note +- Editor `scrollToPos` prop for programmatic scrolling to character positions +- Fixed duplicate `Ctrl+Shift+D` keydown handler in Home.tsx + ### Changed - CI Hardening for `ci.yml` now checks inside app/desktop to confirm build ablility