mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Align tag behavior with Obsidian internals
This commit is contained in:
parent
08ccb61737
commit
1946ecffe0
4 changed files with 59 additions and 36 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import type { App, EventRef } from "obsidian";
|
||||
import { getAllTags, stripHeadingForLink, TFile } from "obsidian";
|
||||
import { stripHeadingForLink, TFile } from "obsidian";
|
||||
|
||||
import type { NoteCandidateProvider, WikiLinkMention } from "../../application/composer/note-context";
|
||||
import type { NoteCandidate } from "../../application/composer/suggestions";
|
||||
|
|
@ -18,6 +18,10 @@ interface EventSource {
|
|||
offref?(ref: EventRef): void;
|
||||
}
|
||||
|
||||
interface MetadataCacheWithTags {
|
||||
getTags?: () => unknown;
|
||||
}
|
||||
|
||||
export class VaultNoteCandidateProvider implements NoteCandidateProvider {
|
||||
private readonly unregisterEvents: (() => void)[] = [];
|
||||
private fileCandidatesCache: FileCandidate[] | null = null;
|
||||
|
|
@ -60,13 +64,7 @@ export class VaultNoteCandidateProvider implements NoteCandidateProvider {
|
|||
}
|
||||
|
||||
private readTags(): string[] {
|
||||
const tags: string[] = [];
|
||||
for (const file of this.app.vault.getMarkdownFiles()) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (!cache) continue;
|
||||
tags.push(...(getAllTags(cache) ?? []));
|
||||
}
|
||||
return normalizedTags(tags);
|
||||
return normalizedTags(metadataCacheTags(this.app.metadataCache));
|
||||
}
|
||||
|
||||
resolveMention(target: string, sourcePath: string): WikiLinkMention | null {
|
||||
|
|
@ -111,6 +109,14 @@ export class VaultNoteCandidateProvider implements NoteCandidateProvider {
|
|||
}
|
||||
}
|
||||
|
||||
function metadataCacheTags(metadataCache: App["metadataCache"]): string[] {
|
||||
const tagIndex = (metadataCache as MetadataCacheWithTags).getTags?.();
|
||||
if (!tagIndex) return [];
|
||||
if (Array.isArray(tagIndex)) return tagIndex.filter((tag): tag is string => typeof tag === "string");
|
||||
if (typeof tagIndex !== "object") return [];
|
||||
return Object.keys(tagIndex);
|
||||
}
|
||||
|
||||
function normalizedTags(tags: readonly string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const normalizedTags: string[] = [];
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ interface StreamMarkdownRenderContext {
|
|||
vaultPath: string;
|
||||
}
|
||||
|
||||
interface ObsidianGlobalSearchPlugin {
|
||||
openGlobalSearch?: (query: string, active?: boolean) => void;
|
||||
}
|
||||
|
||||
interface ObsidianAppWithInternalPlugins extends App {
|
||||
internalPlugins?: {
|
||||
plugins?: {
|
||||
"global-search"?: {
|
||||
instance?: ObsidianGlobalSearchPlugin;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class MarkdownMessageRenderer {
|
||||
private readonly renderGenerations = new WeakMap<HTMLElement, number>();
|
||||
|
||||
|
|
@ -90,7 +104,7 @@ function bindRenderedTags(parent: HTMLElement, context: RenderedMarkdownLinkCont
|
|||
event.preventDefault();
|
||||
const tag = renderedTagName(link);
|
||||
if (!tag) return;
|
||||
void openTagSearch(context.app, tag);
|
||||
openTagSearch(context.app, tag);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -103,18 +117,14 @@ function renderedTagName(link: HTMLAnchorElement): string | null {
|
|||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
async function openTagSearch(app: App, tag: string): Promise<void> {
|
||||
const leaf = app.workspace.getLeftLeaf(false);
|
||||
if (!leaf) {
|
||||
function openTagSearch(app: App, tag: string): void {
|
||||
const query = `tag:#${tag}`;
|
||||
const searchPlugin = (app as ObsidianAppWithInternalPlugins).internalPlugins?.plugins?.["global-search"]?.instance;
|
||||
if (typeof searchPlugin?.openGlobalSearch !== "function") {
|
||||
new Notice("Cannot open Obsidian search.");
|
||||
return;
|
||||
}
|
||||
await leaf.setViewState({
|
||||
type: "search",
|
||||
active: true,
|
||||
state: { query: `tag:#${tag}` },
|
||||
});
|
||||
await app.workspace.revealLeaf(leaf);
|
||||
searchPlugin.openGlobalSearch(query, true);
|
||||
}
|
||||
|
||||
function bindStreamMarkdownFileLinks(parent: HTMLElement, context: StreamMarkdownRenderContext): void {
|
||||
|
|
|
|||
|
|
@ -102,20 +102,27 @@ describe("VaultNoteCandidateProvider", () => {
|
|||
expect(fileToLinktext).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns normalized Obsidian tag candidates from file metadata", () => {
|
||||
it("returns normalized Obsidian tag candidates from the metadata tag index", () => {
|
||||
const getTags = vi.fn(() => ({
|
||||
"#project/codex": 2,
|
||||
"#daily": 1,
|
||||
"#web": 1,
|
||||
}));
|
||||
const app = appFixture({
|
||||
files: [
|
||||
{ basename: "Alpha", path: "Alpha.md", stat: { mtime: 1 } },
|
||||
{ basename: "Beta", path: "Beta.md", stat: { mtime: 2 } },
|
||||
],
|
||||
getTags,
|
||||
tags: new Map([
|
||||
["Alpha.md", { inline: ["#project/codex"], frontmatter: ["daily"] }],
|
||||
["Beta.md", ["#web", "#project/codex"]],
|
||||
["Alpha.md", { inline: ["#should/not/use"], frontmatter: ["ignored"] }],
|
||||
["Beta.md", ["#ignored"]],
|
||||
]),
|
||||
});
|
||||
const provider = new VaultNoteCandidateProvider(app);
|
||||
|
||||
expect(provider.tags()).toEqual(["daily", "project/codex", "web"]);
|
||||
expect(getTags).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("invalidates cached candidates when the vault changes", () => {
|
||||
|
|
@ -229,6 +236,7 @@ function appFixture(
|
|||
linktexts?: Map<string, string>;
|
||||
fileToLinktext?: (file: TFile, sourcePath: string, omitMdExtension?: boolean) => string;
|
||||
headings?: Map<string, { heading: string; level: number }[]>;
|
||||
getTags?: () => unknown;
|
||||
tags?: Map<string, string[] | { inline?: string[]; frontmatter?: string[] }>;
|
||||
activeFile?: TFile | null;
|
||||
activeView?: unknown;
|
||||
|
|
@ -263,6 +271,7 @@ function appFixture(
|
|||
metadataCache: {
|
||||
on: on("metadata"),
|
||||
offref,
|
||||
getTags: options.getTags,
|
||||
getFirstLinkpathDest: options.getFirstLinkpathDest ?? (() => options.linkDestination ?? null),
|
||||
fileToLinktext:
|
||||
options.fileToLinktext ??
|
||||
|
|
@ -278,7 +287,6 @@ function appFixture(
|
|||
on: on("vault"),
|
||||
offref,
|
||||
getFiles: options.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 AppFixture;
|
||||
|
|
|
|||
|
|
@ -198,14 +198,18 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
cleanup();
|
||||
});
|
||||
|
||||
it("opens Obsidian search when rendered tags are clicked", async () => {
|
||||
const searchLeaf = {
|
||||
setViewState: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const revealLeaf = vi.fn().mockResolvedValue(undefined);
|
||||
it("uses Obsidian global search when rendered tags are clicked", async () => {
|
||||
const openGlobalSearch = vi.fn();
|
||||
const context = markdownLinkContext();
|
||||
context.app.workspace.getLeftLeaf = vi.fn(() => searchLeaf);
|
||||
context.app.workspace.revealLeaf = revealLeaf;
|
||||
Object.assign(context.app, {
|
||||
internalPlugins: {
|
||||
plugins: {
|
||||
"global-search": {
|
||||
instance: { openGlobalSearch },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const { link, cleanup } = await renderedTag(context, {
|
||||
cls: "tag",
|
||||
text: "#project/codex",
|
||||
|
|
@ -215,13 +219,8 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
link.click();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(searchLeaf.setViewState).toHaveBeenCalledWith({
|
||||
type: "search",
|
||||
active: true,
|
||||
state: { query: "tag:#project/codex" },
|
||||
});
|
||||
expect(revealLeaf).toHaveBeenCalledWith(searchLeaf);
|
||||
expect(context.app.workspace.getLeftLeaf).toHaveBeenCalledWith(false);
|
||||
expect(openGlobalSearch).toHaveBeenCalledWith("tag:#project/codex", true);
|
||||
expect(context.app.workspace.getLeftLeaf).not.toHaveBeenCalled();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue