mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Suggest non-markdown vault links
This commit is contained in:
parent
b39ff24bbd
commit
ffccdef769
7 changed files with 170 additions and 12 deletions
|
|
@ -11,11 +11,12 @@ export interface WikiLinkMention {
|
|||
export function noteCandidates(app: App): NoteCandidate[] {
|
||||
const sourcePath = app.workspace.getActiveFile()?.path ?? "";
|
||||
const recentPaths = new Map(app.workspace.getLastOpenFiles().map((path, index) => [path, index]));
|
||||
return app.vault.getMarkdownFiles().map((file) => ({
|
||||
return app.vault.getFiles().map((file) => ({
|
||||
basename: file.basename,
|
||||
displayName: displayNameForFile(file),
|
||||
path: file.path,
|
||||
mtime: file.stat.mtime,
|
||||
linktext: app.metadataCache.fileToLinktext(file, sourcePath, true),
|
||||
linktext: linktextForFile(app, file, sourcePath),
|
||||
recentIndex: recentPaths.get(file.path) ?? null,
|
||||
}));
|
||||
}
|
||||
|
|
@ -30,3 +31,15 @@ export function resolveWikiLinkMention(app: App, target: string): WikiLinkMentio
|
|||
if (abstractFile instanceof TFile) return { name: abstractFile.basename, path: abstractFile.path };
|
||||
return null;
|
||||
}
|
||||
|
||||
function linktextForFile(app: App, file: TFile, sourcePath: string): string {
|
||||
const linktext = app.metadataCache.fileToLinktext(file, sourcePath, true);
|
||||
const extension = file.extension.toLowerCase();
|
||||
return extension === "md" || extension.length === 0 || linktext.toLowerCase().endsWith(`.${extension}`)
|
||||
? linktext
|
||||
: `${linktext}.${file.extension}`;
|
||||
}
|
||||
|
||||
function displayNameForFile(file: TFile): string {
|
||||
return file.extension === "md" ? file.basename : file.name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface ComposerSuggestion {
|
|||
|
||||
export interface NoteCandidate {
|
||||
basename: string;
|
||||
displayName: string;
|
||||
path: string;
|
||||
mtime: number;
|
||||
linktext: string;
|
||||
|
|
@ -108,7 +109,7 @@ export function findWikiLinkSuggestions(queryText: string, start: number, notes:
|
|||
const suggestions = query.length === 0 ? emptyWikiLinkSuggestions(notes) : fuzzyWikiLinkSuggestions(query, notes);
|
||||
|
||||
return suggestions.slice(0, 8).map(({ file }) => ({
|
||||
display: file.basename,
|
||||
display: file.displayName,
|
||||
detail: file.path,
|
||||
replacement: `[[${file.linktext}]]`,
|
||||
start,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
nextComposerSuggestionIndex,
|
||||
parseSlashCommand,
|
||||
} from "../../../../src/features/chat/composer/suggestions";
|
||||
import { userInputWithWikiLinkMentions } from "../../../../src/features/chat/composer/wikilink-context";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
@ -67,9 +68,33 @@ function model(name: string, efforts: ReasoningEffort[], overrides: Partial<Mode
|
|||
|
||||
describe("composer suggestions", () => {
|
||||
const notes = [
|
||||
{ basename: "Alpha", path: "thoughts/Alpha.md", mtime: 10, linktext: "thoughts/Alpha", recentIndex: 1 },
|
||||
{ basename: "Alpha", path: "projects/Alpha.md", mtime: 20, linktext: "projects/Alpha", recentIndex: 0 },
|
||||
{ basename: "Beta Note", path: "topics/Beta Note.md", mtime: 30, linktext: "Beta Note", recentIndex: null },
|
||||
{ basename: "Alpha", displayName: "Alpha", path: "thoughts/Alpha.md", mtime: 10, linktext: "thoughts/Alpha", recentIndex: 1 },
|
||||
{ basename: "Alpha", displayName: "Alpha", path: "projects/Alpha.md", mtime: 20, linktext: "projects/Alpha", recentIndex: 0 },
|
||||
{ basename: "Beta Note", displayName: "Beta Note", path: "topics/Beta Note.md", mtime: 30, linktext: "Beta Note", recentIndex: null },
|
||||
{
|
||||
basename: "Projects",
|
||||
displayName: "Projects.base",
|
||||
path: "Bases/Projects.base",
|
||||
mtime: 40,
|
||||
linktext: "Bases/Projects.base",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "Paper",
|
||||
displayName: "Paper.pdf",
|
||||
path: "References/Paper.pdf",
|
||||
mtime: 50,
|
||||
linktext: "References/Paper.pdf",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "Diagram",
|
||||
displayName: "Diagram.png",
|
||||
path: "Assets/Diagram.png",
|
||||
mtime: 60,
|
||||
linktext: "Assets/Diagram.png",
|
||||
recentIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
it("parses supported slash commands only", () => {
|
||||
|
|
@ -105,6 +130,38 @@ describe("composer suggestions", () => {
|
|||
expect(findWikiLinkSuggestions("", 0, notes).map((suggestion) => suggestion.replacement)).toEqual([
|
||||
"[[projects/Alpha]]",
|
||||
"[[thoughts/Alpha]]",
|
||||
"[[Assets/Diagram.png]]",
|
||||
]);
|
||||
});
|
||||
|
||||
it("suggests non-markdown vault files when filtering wikilinks", () => {
|
||||
expect(findWikiLinkSuggestions("projects", 0, notes)[0]).toMatchObject({
|
||||
display: "Projects.base",
|
||||
detail: "Bases/Projects.base",
|
||||
replacement: "[[Bases/Projects.base]]",
|
||||
});
|
||||
expect(findWikiLinkSuggestions("paper", 0, notes)[0]).toMatchObject({
|
||||
display: "Paper.pdf",
|
||||
detail: "References/Paper.pdf",
|
||||
replacement: "[[References/Paper.pdf]]",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps non-markdown wikilink completions compatible with mention parsing", () => {
|
||||
const suggestion = expectPresent(findWikiLinkSuggestions("diagram", 0, notes)[0]);
|
||||
const text = `Please inspect ${suggestion.replacement}`;
|
||||
const input = userInputWithWikiLinkMentions(text, (target) =>
|
||||
target === "Assets/Diagram.png" ? { name: "Diagram", path: "Assets/Diagram.png" } : null,
|
||||
);
|
||||
|
||||
expect(suggestion).toMatchObject({
|
||||
display: "Diagram.png",
|
||||
detail: "Assets/Diagram.png",
|
||||
replacement: "[[Assets/Diagram.png]]",
|
||||
});
|
||||
expect(input).toEqual([
|
||||
{ type: "text", text, text_elements: [] },
|
||||
{ type: "mention", name: "Diagram", path: "Assets/Diagram.png" },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,28 +6,69 @@ import { noteCandidates, resolveWikiLinkMention } from "../../../../src/features
|
|||
describe("Obsidian composer context", () => {
|
||||
it("builds note candidates from markdown files", () => {
|
||||
const app = appFixture({
|
||||
markdownFiles: [
|
||||
files: [
|
||||
{ basename: "Alpha", path: "notes/Alpha.md", stat: { mtime: 100 } },
|
||||
{ basename: "Beta", path: "Beta.md", stat: { mtime: 200 } },
|
||||
{ basename: "Daily", path: "Personal/Daily Notes/Daily.md", stat: { mtime: 300 } },
|
||||
{ basename: "Projects", path: "Bases/Projects.base", stat: { mtime: 400 } },
|
||||
{ basename: "Paper", path: "References/Paper.pdf", stat: { mtime: 500 } },
|
||||
{ basename: "Diagram", path: "Assets/Diagram.png", stat: { mtime: 600 } },
|
||||
{ basename: "LICENSE", path: "Attachments/LICENSE", stat: { mtime: 700 } },
|
||||
],
|
||||
lastOpenFiles: ["Beta.md"],
|
||||
linktexts: new Map([
|
||||
["notes/Alpha.md", "Alpha"],
|
||||
["Personal/Daily Notes/Daily.md", "Personal/Daily Notes/Daily"],
|
||||
["Bases/Projects.base", "Bases/Projects"],
|
||||
["References/Paper.pdf", "References/Paper"],
|
||||
["Assets/Diagram.png", "Assets/Diagram.png"],
|
||||
["Attachments/LICENSE", "Attachments/LICENSE"],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(noteCandidates(app)).toEqual([
|
||||
{ basename: "Alpha", path: "notes/Alpha.md", mtime: 100, linktext: "Alpha", recentIndex: null },
|
||||
{ basename: "Beta", path: "Beta.md", mtime: 200, linktext: "Beta", recentIndex: 0 },
|
||||
{ basename: "Alpha", displayName: "Alpha", path: "notes/Alpha.md", mtime: 100, linktext: "Alpha", recentIndex: null },
|
||||
{ basename: "Beta", displayName: "Beta", path: "Beta.md", mtime: 200, linktext: "Beta", recentIndex: 0 },
|
||||
{
|
||||
basename: "Daily",
|
||||
displayName: "Daily",
|
||||
path: "Personal/Daily Notes/Daily.md",
|
||||
mtime: 300,
|
||||
linktext: "Personal/Daily Notes/Daily",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "Projects",
|
||||
displayName: "Projects.base",
|
||||
path: "Bases/Projects.base",
|
||||
mtime: 400,
|
||||
linktext: "Bases/Projects.base",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "Paper",
|
||||
displayName: "Paper.pdf",
|
||||
path: "References/Paper.pdf",
|
||||
mtime: 500,
|
||||
linktext: "References/Paper.pdf",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "Diagram",
|
||||
displayName: "Diagram.png",
|
||||
path: "Assets/Diagram.png",
|
||||
mtime: 600,
|
||||
linktext: "Assets/Diagram.png",
|
||||
recentIndex: null,
|
||||
},
|
||||
{
|
||||
basename: "LICENSE",
|
||||
displayName: "LICENSE",
|
||||
path: "Attachments/LICENSE",
|
||||
mtime: 700,
|
||||
linktext: "Attachments/LICENSE",
|
||||
recentIndex: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -64,6 +105,18 @@ describe("Obsidian composer context", () => {
|
|||
expect(resolveWikiLinkMention(app, "Project")).toEqual({ name: "Project", path: "notes/Project.md" });
|
||||
expect(getFirstLinkpathDest).toHaveBeenCalledWith("Project", "Daily/Today.md");
|
||||
});
|
||||
|
||||
it("resolves non-markdown wikilinks through Obsidian metadata", () => {
|
||||
const linked = tFile("Bases/Projects.base", "Projects");
|
||||
const getFirstLinkpathDest = vi.fn(() => linked);
|
||||
const app = appFixture({
|
||||
activePath: "Daily/Today.md",
|
||||
getFirstLinkpathDest,
|
||||
});
|
||||
|
||||
expect(resolveWikiLinkMention(app, "Bases/Projects.base")).toEqual({ name: "Projects", path: "Bases/Projects.base" });
|
||||
expect(getFirstLinkpathDest).toHaveBeenCalledWith("Bases/Projects.base", "Daily/Today.md");
|
||||
});
|
||||
});
|
||||
|
||||
function appFixture(options: {
|
||||
|
|
@ -71,7 +124,7 @@ function appFixture(options: {
|
|||
linkDestination?: TFile | null;
|
||||
getFirstLinkpathDest?: (target: string, sourcePath: string) => TFile | null;
|
||||
lastOpenFiles?: string[];
|
||||
markdownFiles?: { basename: string; path: string; stat: { mtime: number } }[];
|
||||
files?: { basename: string; path: string; stat: { mtime: number } }[];
|
||||
abstractFiles?: Map<string, TFile>;
|
||||
linktexts?: Map<string, string>;
|
||||
}): App {
|
||||
|
|
@ -86,12 +139,20 @@ function appFixture(options: {
|
|||
options.linktexts?.get(file.path) ?? (omitMdExtension === true ? file.path.replace(/\.md$/i, "") : file.path),
|
||||
},
|
||||
vault: {
|
||||
getMarkdownFiles: () => options.markdownFiles ?? [],
|
||||
getFiles: () => vaultFiles(options.files ?? []),
|
||||
getMarkdownFiles: () => vaultFiles(options.files ?? []).filter((file) => file.path.toLowerCase().endsWith(".md")),
|
||||
getAbstractFileByPath: (path: string) => options.abstractFiles?.get(path) ?? null,
|
||||
},
|
||||
} as unknown as App;
|
||||
}
|
||||
|
||||
function tFile(path: string, basename: string): TFile {
|
||||
return Object.assign(new TFile(), { path, basename });
|
||||
const name = path.split("/").pop() ?? path;
|
||||
const extensionStart = name.lastIndexOf(".");
|
||||
const extension = extensionStart === -1 ? "" : name.slice(extensionStart + 1);
|
||||
return Object.assign(new TFile(), { path, basename, extension, name });
|
||||
}
|
||||
|
||||
function vaultFiles(files: { basename: string; path: string; stat: { mtime: number } }[]): TFile[] {
|
||||
return files.map((file) => Object.assign(tFile(file.path, file.basename), { stat: file.stat }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,30 @@ describe("wikilink context", () => {
|
|||
expect(input).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("resolves aliases and subpaths from non-markdown wikilinks by target", () => {
|
||||
const text = "Open [[Bases/Projects.base|Projects]], [[References/Paper.pdf]], and [[Assets/Diagram.png#crop|Diagram]].";
|
||||
const input = userInputWithWikiLinkMentions(text, (target) => {
|
||||
const mentions = new Map([
|
||||
["Bases/Projects.base", { name: "Projects", path: "Bases/Projects.base" }],
|
||||
["References/Paper.pdf", { name: "Paper", path: "References/Paper.pdf" }],
|
||||
["Assets/Diagram.png", { name: "Diagram", path: "Assets/Diagram.png" }],
|
||||
]);
|
||||
return mentions.get(target) ?? null;
|
||||
});
|
||||
|
||||
expect(parsedWikiLinks(text)).toEqual([
|
||||
{ raw: "Bases/Projects.base|Projects", target: "Bases/Projects.base", subpath: "", display: "Projects" },
|
||||
{ raw: "References/Paper.pdf", target: "References/Paper.pdf", subpath: "", display: "" },
|
||||
{ raw: "Assets/Diagram.png#crop|Diagram", target: "Assets/Diagram.png", subpath: "#crop", display: "Diagram" },
|
||||
]);
|
||||
expect(input).toEqual([
|
||||
{ type: "text", text, text_elements: [] },
|
||||
{ type: "mention", name: "Projects", path: "Bases/Projects.base" },
|
||||
{ type: "mention", name: "Paper", path: "References/Paper.pdf" },
|
||||
{ type: "mention", name: "Diagram", path: "Assets/Diagram.png" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("deduplicates mentions by resolved path", () => {
|
||||
const text = "Read [[Alpha]], [[Alpha#Heading]], and [[Alias|A]].";
|
||||
const input = userInputWithWikiLinkMentions(text, (target) =>
|
||||
|
|
|
|||
|
|
@ -801,6 +801,7 @@ async function chatView(
|
|||
},
|
||||
vault: {
|
||||
on: vi.fn(() => ({})),
|
||||
getFiles: vi.fn(() => []),
|
||||
getMarkdownFiles: vi.fn(() => []),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) {
|
|||
},
|
||||
vault: {
|
||||
on: vi.fn(() => ({})),
|
||||
getFiles: vi.fn(() => []),
|
||||
getMarkdownFiles: vi.fn(() => []),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue