diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index bf9266ff..1e422c93 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -60,7 +60,7 @@ pub struct VaultEntry { pub color: Option, /// Display order for Type entries in sidebar (lower = higher). None = use default order. pub order: Option, - /// All wikilink targets found in the note content (body + frontmatter). + /// All wikilink targets found in the note body (excludes frontmatter). /// Extracted from `[[target]]` and `[[target|display]]` patterns. #[serde(rename = "outgoingLinks", default)] pub outgoing_links: Vec, @@ -268,7 +268,7 @@ pub fn parse_md_file(path: &Path) -> Result { let title = extract_title(&parsed.content, &filename); let snippet = extract_snippet(&content); - let outgoing_links = extract_outgoing_links(&content); + let outgoing_links = extract_outgoing_links(&parsed.content); let (modified_at, file_size) = read_file_metadata(path)?; let created_at = parse_created_at(&frontmatter); let is_a = resolve_is_a(frontmatter.is_a, path); @@ -951,11 +951,11 @@ References: } #[test] - fn test_outgoing_links_includes_frontmatter_wikilinks() { + fn test_outgoing_links_excludes_frontmatter_wikilinks() { let dir = TempDir::new().unwrap(); let content = "---\nHas:\n - \"[[task/design]]\"\n---\n# Note\n\nSee [[person/bob]]."; let entry = parse_test_entry(&dir, "note/test.md", content); - assert!(entry.outgoing_links.contains(&"task/design".to_string())); + assert!(!entry.outgoing_links.contains(&"task/design".to_string())); assert!(entry.outgoing_links.contains(&"person/bob".to_string())); } diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index 9dda1957..b95a18c7 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -575,6 +575,47 @@ Status: Active expect(screen.getByText(/via Topics/)).toBeInTheDocument() }) + it('excludes entries from backlinks when already shown in referenced-by', () => { + const noteA: VaultEntry = { + path: '/Users/luca/Laputa/essay/on-writing.md', + filename: 'on-writing.md', + title: 'On Writing Well', + isA: 'Essay', + aliases: [], + belongsTo: ['[[responsibility/grow-newsletter]]'], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: 1707900000, + createdAt: null, + fileSize: 300, + snippet: '', + relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] }, + icon: null, + color: null, + order: null, + // Body text also links to grow-newsletter + outgoingLinks: ['responsibility/grow-newsletter'], + } + render( + + ) + // noteA shows in Referenced By (via Belongs to) + expect(screen.getByText(/via Belongs to/)).toBeInTheDocument() + expect(screen.getByText('On Writing Well')).toBeInTheDocument() + // But NOT in Backlinks (even though outgoingLinks matches) + expect(screen.getByText('No backlinks')).toBeInTheDocument() + }) + it('does not show self-references', () => { const selfRef: VaultEntry = { ...targetEntry, diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index ceb4705b..1e549065 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -25,7 +25,7 @@ interface InspectorProps { onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise } -function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[]): VaultEntry[] { +function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[], referencedBy: ReferencedByItem[]): VaultEntry[] { return useMemo(() => { if (!entry) return [] const matchTargets = new Set([ @@ -34,13 +34,16 @@ function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[]): VaultEnt entry.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, ''), ]) + const referencedByPaths = new Set(referencedBy.map((item) => item.entry.path)) + return entries.filter((e) => { if (e.path === entry.path) return false + if (referencedByPaths.has(e.path)) return false return e.outgoingLinks.some((target) => matchTargets.has(target) || matchTargets.has(target.split('/').pop() ?? '') ) }) - }, [entry, entries]) + }, [entry, entries, referencedBy]) } function useReferencedBy(entry: VaultEntry | null, entries: VaultEntry[]): ReferencedByItem[] { @@ -118,8 +121,8 @@ export function Inspector({ collapsed, onToggle, entry, content, entries, gitHistory, onNavigate, onViewCommitDiff, onUpdateFrontmatter, onDeleteProperty, onAddProperty, }: InspectorProps) { - const backlinks = useBacklinks(entry, entries) const referencedBy = useReferencedBy(entry, entries) + const backlinks = useBacklinks(entry, entries, referencedBy) const frontmatter = useMemo(() => parseFrontmatter(content), [content]) const typeEntryMap = useMemo(() => { const map: Record = {}