mirror of
https://github.com/aaronsb/obsidian-mcp-plugin.git
synced 2026-07-22 06:45:14 +00:00
Merge pull request #207 from fa1k3/fix/graph-index-titles
Use parent folder titles for index graph nodes
This commit is contained in:
commit
4fc596b9eb
3 changed files with 137 additions and 11 deletions
|
|
@ -274,7 +274,7 @@ export class GraphSearchTool {
|
|||
const paths = rawPaths.map(pathList =>
|
||||
pathList.map(filePath => ({
|
||||
path: filePath,
|
||||
title: filePath.replace(/\.md$/, '').split('/').pop() || filePath
|
||||
title: this.graphTraversal.getNodeTitleForPath(filePath)
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -324,12 +324,15 @@ export class GraphSearchTool {
|
|||
|
||||
const stats = this.graphTraversal.getNodeStatistics(params.sourcePath);
|
||||
const file = this.app.vault.getAbstractFileByPath(params.sourcePath);
|
||||
const title = file instanceof TFile
|
||||
? this.graphTraversal.getNodeTitle(file)
|
||||
: params.sourcePath;
|
||||
|
||||
return {
|
||||
operation: 'statistics',
|
||||
sourcePath: params.sourcePath,
|
||||
statistics: stats,
|
||||
message: `Link statistics for ${file?.name || params.sourcePath}`,
|
||||
message: `Link statistics for ${title}`,
|
||||
workflow: {
|
||||
message: 'Statistics retrieved. You can explore the actual links or find connected nodes.',
|
||||
suggested_next: [
|
||||
|
|
@ -371,7 +374,7 @@ export class GraphSearchTool {
|
|||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
nodes.push({
|
||||
path: edge.source,
|
||||
title: file.name.replace(/\.md$/, ''),
|
||||
title: this.graphTraversal.getNodeTitle(file),
|
||||
type: 'file',
|
||||
tags: cache?.tags?.map(t => t.tag),
|
||||
links: {
|
||||
|
|
@ -427,7 +430,7 @@ export class GraphSearchTool {
|
|||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
nodes.push({
|
||||
path: edge.target,
|
||||
title: file.name.replace(/\.md$/, ''),
|
||||
title: this.graphTraversal.getNodeTitle(file),
|
||||
type: 'file',
|
||||
tags: cache?.tags?.map(t => t.tag),
|
||||
links: {
|
||||
|
|
@ -464,4 +467,4 @@ export class GraphSearchTool {
|
|||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,31 @@ export interface GraphTraversalResult {
|
|||
export class GraphTraversal {
|
||||
constructor(private app: App) {}
|
||||
|
||||
/**
|
||||
* Get a human-friendly graph node title for a file.
|
||||
*
|
||||
* Vaults commonly use <topic>/index.md as landing pages. Showing every
|
||||
* such node as "index" makes graph results ambiguous, so use the parent
|
||||
* folder name for index files when available.
|
||||
*/
|
||||
getNodeTitle(file: TFile): string {
|
||||
if (file.basename === 'index' && file.parent?.name) {
|
||||
return file.parent.name;
|
||||
}
|
||||
return file.basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to a graph node title, falling back to the path basename.
|
||||
*/
|
||||
getNodeTitleForPath(filePath: string): string {
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (file instanceof TFile) {
|
||||
return this.getNodeTitle(file);
|
||||
}
|
||||
return filePath.replace(/\.md$/, '').split('/').pop() || filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes (files) in the vault
|
||||
*/
|
||||
|
|
@ -63,7 +88,7 @@ export class GraphTraversal {
|
|||
return files.map(file => ({
|
||||
file,
|
||||
path: file.path,
|
||||
title: file.basename,
|
||||
title: this.getNodeTitle(file),
|
||||
metadata: this.app.metadataCache.getFileCache(file) || undefined
|
||||
}));
|
||||
}
|
||||
|
|
@ -223,7 +248,7 @@ export class GraphTraversal {
|
|||
const node: GraphNode = {
|
||||
file,
|
||||
path: file.path,
|
||||
title: file.basename,
|
||||
title: this.getNodeTitle(file),
|
||||
metadata: this.app.metadataCache.getFileCache(file) || undefined
|
||||
};
|
||||
|
||||
|
|
@ -378,7 +403,7 @@ export class GraphTraversal {
|
|||
const neighbors: GraphNode[] = recentFiles.map(file => ({
|
||||
file,
|
||||
path: file.path,
|
||||
title: file.basename,
|
||||
title: this.getNodeTitle(file),
|
||||
metadata: this.app.metadataCache.getFileCache(file) || undefined
|
||||
}));
|
||||
|
||||
|
|
@ -413,7 +438,7 @@ export class GraphTraversal {
|
|||
const node: GraphNode = {
|
||||
file,
|
||||
path: file.path,
|
||||
title: file.basename,
|
||||
title: this.getNodeTitle(file),
|
||||
metadata: this.app.metadataCache.getFileCache(file) || undefined
|
||||
};
|
||||
|
||||
|
|
@ -432,7 +457,7 @@ export class GraphTraversal {
|
|||
neighbors.push({
|
||||
file: neighborFile,
|
||||
path: neighborFile.path,
|
||||
title: neighborFile.basename,
|
||||
title: this.getNodeTitle(neighborFile),
|
||||
metadata: this.app.metadataCache.getFileCache(neighborFile) || undefined
|
||||
});
|
||||
}
|
||||
|
|
@ -470,4 +495,4 @@ export class GraphTraversal {
|
|||
tagCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
tests/graph-node-title.test.ts
Normal file
98
tests/graph-node-title.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { App, TFile } from 'obsidian';
|
||||
import { GraphSearchTool } from '../src/tools/graph-search';
|
||||
import { GraphTraversal } from '../src/utils/graph-traversal';
|
||||
import { ObsidianAPI } from '../src/utils/obsidian-api';
|
||||
|
||||
function makeFile(path: string, basename: string, parentName?: string): TFile {
|
||||
const file = new TFile();
|
||||
file.path = path;
|
||||
file.name = `${basename}.md`;
|
||||
file.basename = basename;
|
||||
file.extension = 'md';
|
||||
(file as any).parent = parentName
|
||||
? { name: parentName }
|
||||
: null;
|
||||
return file;
|
||||
}
|
||||
|
||||
describe('graph node titles', () => {
|
||||
let app: App;
|
||||
let traversal: GraphTraversal;
|
||||
let search: GraphSearchTool;
|
||||
let indexFile: TFile;
|
||||
let regularFile: TFile;
|
||||
let sourceFile: TFile;
|
||||
|
||||
beforeEach(() => {
|
||||
indexFile = makeFile('topics/rendering/index.md', 'index', 'rendering');
|
||||
regularFile = makeFile('topics/rendering/overview.md', 'overview', 'rendering');
|
||||
sourceFile = makeFile('source.md', 'source');
|
||||
|
||||
const filesByPath = new Map([
|
||||
[indexFile.path, indexFile],
|
||||
[regularFile.path, regularFile],
|
||||
[sourceFile.path, sourceFile]
|
||||
]);
|
||||
|
||||
app = new App();
|
||||
(app as any).metadataCache = {
|
||||
resolvedLinks: {
|
||||
'source.md': { 'topics/rendering/index.md': 1 },
|
||||
'topics/rendering/index.md': { 'topics/rendering/overview.md': 1 }
|
||||
},
|
||||
unresolvedLinks: {},
|
||||
getFileCache: jest.fn().mockReturnValue({ tags: [] })
|
||||
};
|
||||
app.vault.getFiles = jest.fn(() => [indexFile, regularFile, sourceFile]);
|
||||
app.vault.getAbstractFileByPath = jest.fn((path: string) => filesByPath.get(path) ?? null);
|
||||
|
||||
traversal = new GraphTraversal(app);
|
||||
search = new GraphSearchTool({} as ObsidianAPI, app);
|
||||
});
|
||||
|
||||
it('uses the parent folder as the graph title for index files', () => {
|
||||
expect(traversal.getNodeTitle(indexFile)).toBe('rendering');
|
||||
expect(traversal.getNodeTitle(regularFile)).toBe('overview');
|
||||
});
|
||||
|
||||
it('uses resolved graph titles in vault node listings', () => {
|
||||
const nodes = traversal.getAllNodes();
|
||||
|
||||
expect(nodes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' }),
|
||||
expect.objectContaining({ path: 'topics/rendering/overview.md', title: 'overview' })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('uses resolved graph titles in forwardlink results', () => {
|
||||
const result = search.search({ operation: 'forwardlinks', sourcePath: 'source.md' });
|
||||
|
||||
expect(result.nodes).toEqual([
|
||||
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses resolved graph titles in backlink results', () => {
|
||||
const result = search.search({ operation: 'backlinks', sourcePath: 'topics/rendering/overview.md' });
|
||||
|
||||
expect(result.nodes).toEqual([
|
||||
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses resolved graph titles in path results', () => {
|
||||
const result = search.search({
|
||||
operation: 'path',
|
||||
sourcePath: 'source.md',
|
||||
targetPath: 'topics/rendering/overview.md'
|
||||
});
|
||||
|
||||
expect(result.paths?.[0]).toEqual([
|
||||
{ path: 'source.md', title: 'source' },
|
||||
{ path: 'topics/rendering/index.md', title: 'rendering' },
|
||||
{ path: 'topics/rendering/overview.md', title: 'overview' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue