mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
fix heading task link labels
This commit is contained in:
parent
cac4f5e170
commit
77dd5d9db3
6 changed files with 290 additions and 25 deletions
|
|
@ -51,6 +51,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
|
|||
- (#2039) Fixed broken images on the Workflows and Calendar Setup documentation pages. Thanks to @chmac for reporting this.
|
||||
- (#2045) Fixed custom modal fields assigned to Basic Information not appearing in task creation or edit modals. Thanks to @chmac for reporting this.
|
||||
- (#2063) Fixed project autocomplete storing ambiguous project links without the selected note's folder path. Thanks to @chmac for reporting this.
|
||||
- (#2072) Fixed inline task links to headings so they show both the task title and heading instead of only the heading. Thanks to @spasche for reporting this.
|
||||
|
||||
## Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {
|
|||
import { TaskLinkWidget } from "./TaskLinkWidget";
|
||||
import { createTaskNotesLogger } from "../utils/tasknotesLogger";
|
||||
import {
|
||||
extractTaskLinkSubpath,
|
||||
formatTaskLinkSubpathDisplayText,
|
||||
isImplicitTaskLinkDisplayText,
|
||||
resolveTaskLinkDisplayText,
|
||||
} from "./taskLinkDisplayText";
|
||||
|
|
@ -20,6 +22,7 @@ const tasknotesLogger = createTaskNotesLogger({ tag: "Editor/ReadingModeTaskLink
|
|||
export interface ReadingModeSourceLink {
|
||||
target: string;
|
||||
hasAlias: boolean;
|
||||
subpath?: string;
|
||||
}
|
||||
|
||||
export interface ReadingModeSourceLinkCursor {
|
||||
|
|
@ -91,6 +94,7 @@ export function parseReadingModeSourceLinks(sourceText: string): ReadingModeSour
|
|||
links.push({
|
||||
target,
|
||||
hasAlias: pipeIndex !== -1,
|
||||
subpath: extractTaskLinkSubpath(rawTarget),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +153,7 @@ export function shouldSkipReadingModeTaskLinkOverlay(options: {
|
|||
hasExplicitAlias: boolean;
|
||||
linkText: string;
|
||||
originalLinkPath: string;
|
||||
taskSubpath?: string;
|
||||
taskPath?: string;
|
||||
taskTitle: string;
|
||||
}): boolean {
|
||||
|
|
@ -156,7 +161,21 @@ export function shouldSkipReadingModeTaskLinkOverlay(options: {
|
|||
if (options.hasExplicitAlias) return true;
|
||||
|
||||
const currentText = options.linkText.trim();
|
||||
if (isImplicitTaskLinkDisplayText(currentText, options.taskPath, options.originalLinkPath)) {
|
||||
const subpathDisplayText = formatTaskLinkSubpathDisplayText(
|
||||
options.taskTitle,
|
||||
options.taskSubpath ?? extractTaskLinkSubpath(options.originalLinkPath)
|
||||
);
|
||||
if (subpathDisplayText && currentText === subpathDisplayText) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
isImplicitTaskLinkDisplayText(
|
||||
currentText,
|
||||
options.taskPath,
|
||||
options.originalLinkPath,
|
||||
options.taskTitle
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return currentText !== options.originalLinkPath && currentText !== options.taskTitle;
|
||||
|
|
@ -222,7 +241,8 @@ export class ReadingModeTaskLinkProcessor {
|
|||
linkEl,
|
||||
ctx.sourcePath,
|
||||
"internal",
|
||||
sourceLink?.hasAlias ?? false
|
||||
sourceLink?.hasAlias ?? false,
|
||||
sourceLink?.subpath
|
||||
);
|
||||
}
|
||||
// Process other links that might be markdown links to internal files
|
||||
|
|
@ -236,7 +256,8 @@ export class ReadingModeTaskLinkProcessor {
|
|||
linkEl,
|
||||
ctx.sourcePath,
|
||||
"external",
|
||||
sourceLink?.hasAlias ?? false
|
||||
sourceLink?.hasAlias ?? false,
|
||||
sourceLink?.subpath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +271,8 @@ export class ReadingModeTaskLinkProcessor {
|
|||
linkEl: HTMLAnchorElement,
|
||||
sourcePath: string,
|
||||
linkType: "internal" | "external",
|
||||
hasExplicitAlias: boolean
|
||||
hasExplicitAlias: boolean,
|
||||
implicitSubpath?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get the link path from the href attribute
|
||||
|
|
@ -294,7 +316,13 @@ export class ReadingModeTaskLinkProcessor {
|
|||
if (!taskInfo) return;
|
||||
|
||||
// Create a task widget and replace the link
|
||||
await this.replaceWithTaskWidget(linkEl, taskInfo, linkPath, hasExplicitAlias);
|
||||
await this.replaceWithTaskWidget(
|
||||
linkEl,
|
||||
taskInfo,
|
||||
linkPath,
|
||||
hasExplicitAlias,
|
||||
implicitSubpath
|
||||
);
|
||||
} catch (error) {
|
||||
tasknotesLogger.debug("Error processing link in reading mode:", {
|
||||
category: "persistence",
|
||||
|
|
@ -511,11 +539,16 @@ export class ReadingModeTaskLinkProcessor {
|
|||
linkEl: HTMLAnchorElement,
|
||||
taskInfo: TaskInfo,
|
||||
originalLinkPath: string,
|
||||
hasExplicitAlias: boolean
|
||||
hasExplicitAlias: boolean,
|
||||
implicitSubpath?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get the original link text for display
|
||||
const originalText = linkEl.textContent || `[[${originalLinkPath}]]`;
|
||||
const implicitSubpathDisplayText = formatTaskLinkSubpathDisplayText(
|
||||
taskInfo.title,
|
||||
implicitSubpath ?? extractTaskLinkSubpath(originalLinkPath)
|
||||
);
|
||||
|
||||
// Check for alias exclusion
|
||||
if (
|
||||
|
|
@ -524,6 +557,7 @@ export class ReadingModeTaskLinkProcessor {
|
|||
hasExplicitAlias,
|
||||
linkText: linkEl.textContent || "",
|
||||
originalLinkPath,
|
||||
taskSubpath: implicitSubpath,
|
||||
taskPath: taskInfo.path,
|
||||
taskTitle: taskInfo.title,
|
||||
})
|
||||
|
|
@ -536,6 +570,8 @@ export class ReadingModeTaskLinkProcessor {
|
|||
const linkContent = linkEl.textContent || "";
|
||||
if (hasExplicitAlias) {
|
||||
displayText = linkContent.trim() || undefined;
|
||||
} else if (implicitSubpathDisplayText) {
|
||||
displayText = implicitSubpathDisplayText;
|
||||
} else if (linkContent !== originalLinkPath && linkContent !== taskInfo.title) {
|
||||
displayText = resolveTaskLinkDisplayText(
|
||||
linkContent,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,23 @@ import {
|
|||
import { TaskLinkDetectionService } from "../services/TaskLinkDetectionService";
|
||||
import { TaskLinkWidget } from "./TaskLinkWidget";
|
||||
import { createTaskNotesLogger } from "../utils/tasknotesLogger";
|
||||
import { resolveTaskLinkDisplayText } from "./taskLinkDisplayText";
|
||||
import {
|
||||
formatTaskLinkSubpathDisplayText,
|
||||
resolveTaskLinkDisplayText,
|
||||
} from "./taskLinkDisplayText";
|
||||
|
||||
const tasknotesLogger = createTaskNotesLogger({ tag: "Editor/TaskLinkOverlay" });
|
||||
|
||||
// Define a state effect for task updates
|
||||
const taskUpdateEffect = StateEffect.define<{ taskPath?: string }>();
|
||||
|
||||
interface ParsedTaskLink {
|
||||
linkPath: string;
|
||||
displayText?: string;
|
||||
subpath?: string;
|
||||
hasExplicitAlias: boolean;
|
||||
}
|
||||
|
||||
// Create a ViewPlugin factory that takes the plugin as a parameter
|
||||
export function createTaskLinkViewPlugin(plugin: TaskNotesPlugin) {
|
||||
// Track widget instances for updates
|
||||
|
|
@ -348,14 +358,23 @@ export function buildTaskLinkDecorations(
|
|||
const widgetKey = `${resolvedPath}-${link.start}-${link.end}`;
|
||||
|
||||
// Always create a new widget with the current task info
|
||||
const displayText =
|
||||
link.type === "markdown"
|
||||
let displayText: string | undefined;
|
||||
if (link.type === "markdown") {
|
||||
displayText = parsed.displayText
|
||||
? resolveTaskLinkDisplayText(
|
||||
parsed.displayText,
|
||||
taskInfo.path,
|
||||
linkPath
|
||||
)
|
||||
: parsed.displayText;
|
||||
: formatTaskLinkSubpathDisplayText(taskInfo.title, parsed.subpath);
|
||||
} else if (parsed.hasExplicitAlias) {
|
||||
displayText = parsed.displayText;
|
||||
} else {
|
||||
displayText = formatTaskLinkSubpathDisplayText(
|
||||
taskInfo.title,
|
||||
parsed.subpath
|
||||
);
|
||||
}
|
||||
const newWidget = new TaskLinkWidget(
|
||||
taskInfo,
|
||||
plugin,
|
||||
|
|
@ -423,7 +442,7 @@ export function buildTaskLinkDecorations(
|
|||
// Synchronous helper functions for State Field context
|
||||
function parseWikilinkSync(
|
||||
wikilinkText: string
|
||||
): { linkPath: string; displayText?: string } | null {
|
||||
): ParsedTaskLink | null {
|
||||
// Validate input
|
||||
if (!wikilinkText || typeof wikilinkText !== "string") {
|
||||
return null;
|
||||
|
|
@ -465,6 +484,8 @@ function parseWikilinkSync(
|
|||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: aliasPart,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -478,13 +499,14 @@ function parseWikilinkSync(
|
|||
|
||||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: parsed.subpath || undefined,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parseMarkdownLinkSync(
|
||||
markdownLinkText: string
|
||||
): { linkPath: string; displayText?: string } | null {
|
||||
): ParsedTaskLink | null {
|
||||
// Validate input
|
||||
if (!markdownLinkText || typeof markdownLinkText !== "string") {
|
||||
return null;
|
||||
|
|
@ -528,7 +550,9 @@ function parseMarkdownLinkSync(
|
|||
|
||||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: displayText || parsed.subpath || undefined,
|
||||
displayText: displayText || undefined,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: Boolean(displayText),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,46 @@ function normalizePathLikeLabel(value: string | undefined): string {
|
|||
return withoutLeadingSlash.replace(/\.md$/i, "").trim();
|
||||
}
|
||||
|
||||
function normalizeSubpathLabel(value: string | undefined): string {
|
||||
if (!value) return "";
|
||||
const decoded = safeDecodeURIComponent(value).trim();
|
||||
return decoded.replace(/^[#^]+/, "").trim();
|
||||
}
|
||||
|
||||
function basename(value: string): string {
|
||||
const normalized = normalizePathLikeLabel(value);
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
return parts[parts.length - 1] ?? normalized;
|
||||
}
|
||||
|
||||
export function extractTaskLinkSubpath(linkPath: string | undefined): string | undefined {
|
||||
if (!linkPath) return undefined;
|
||||
const decoded = safeDecodeURIComponent(stripMarkdownPathBrackets(linkPath));
|
||||
const subpathIndex = decoded.search(/[#^]/);
|
||||
if (subpathIndex === -1) return undefined;
|
||||
return normalizeSubpathLabel(decoded.slice(subpathIndex)) || undefined;
|
||||
}
|
||||
|
||||
export function formatTaskLinkSubpathDisplayText(
|
||||
taskTitle: string | undefined,
|
||||
subpath: string | undefined
|
||||
): string | undefined {
|
||||
const normalizedSubpath = normalizeSubpathLabel(subpath);
|
||||
if (!normalizedSubpath) return undefined;
|
||||
|
||||
const normalizedTitle = taskTitle?.trim();
|
||||
if (!normalizedTitle) return normalizedSubpath;
|
||||
|
||||
return `${normalizedTitle} > ${normalizedSubpath}`;
|
||||
}
|
||||
|
||||
export function isImplicitTaskLinkDisplayText(
|
||||
displayText: string | undefined,
|
||||
taskPath: string | undefined,
|
||||
linkPath?: string
|
||||
linkPath?: string,
|
||||
taskTitle?: string
|
||||
): boolean {
|
||||
const normalizedDisplay = normalizePathLikeLabel(displayText);
|
||||
const normalizedDisplay = normalizePathLikeLabel(displayText) || normalizeSubpathLabel(displayText);
|
||||
if (!normalizedDisplay || !taskPath) return false;
|
||||
|
||||
const normalizedTaskPath = normalizePathLikeLabel(taskPath);
|
||||
|
|
@ -48,6 +76,15 @@ export function isImplicitTaskLinkDisplayText(
|
|||
candidates.add(basename(normalizedLinkPath));
|
||||
}
|
||||
|
||||
const normalizedSubpath = extractTaskLinkSubpath(linkPath);
|
||||
if (normalizedSubpath) {
|
||||
candidates.add(normalizedSubpath);
|
||||
const formattedSubpath = formatTaskLinkSubpathDisplayText(taskTitle, normalizedSubpath);
|
||||
if (formattedSubpath) {
|
||||
candidates.add(formattedSubpath);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.has(normalizedDisplay);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { TFile, parseLinktext } from "obsidian";
|
|||
import { TaskInfo } from "../types";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { createTaskNotesLogger, type TaskNotesLogger } from "../utils/tasknotesLogger";
|
||||
import { resolveTaskLinkDisplayText } from "../editor/taskLinkDisplayText";
|
||||
import {
|
||||
formatTaskLinkSubpathDisplayText,
|
||||
resolveTaskLinkDisplayText,
|
||||
} from "../editor/taskLinkDisplayText";
|
||||
|
||||
export interface TaskLinkInfo {
|
||||
isValidTaskLink: boolean;
|
||||
|
|
@ -11,6 +14,13 @@ export interface TaskLinkInfo {
|
|||
displayText?: string;
|
||||
}
|
||||
|
||||
interface ParsedTaskLink {
|
||||
linkPath: string;
|
||||
displayText?: string;
|
||||
subpath?: string;
|
||||
hasExplicitAlias: boolean;
|
||||
}
|
||||
|
||||
export class TaskLinkDetectionService {
|
||||
private plugin: TaskNotesPlugin;
|
||||
private linkCache = new Map<string, { result: TaskLinkInfo; lastModified: number }>();
|
||||
|
|
@ -75,10 +85,13 @@ export class TaskLinkDetectionService {
|
|||
try {
|
||||
const taskInfo = await this.plugin.cacheManager.getTaskInfo(resolvedPath);
|
||||
if (taskInfo) {
|
||||
const displayText =
|
||||
linkType === "markdown"
|
||||
? resolveTaskLinkDisplayText(parsedDisplayText, taskInfo.path, linkPath)
|
||||
: parsedDisplayText;
|
||||
const displayText = this.resolveDisplayText(
|
||||
parsed,
|
||||
linkType,
|
||||
taskInfo,
|
||||
linkPath,
|
||||
parsedDisplayText
|
||||
);
|
||||
const result: TaskLinkInfo = {
|
||||
isValidTaskLink: true,
|
||||
taskPath: resolvedPath,
|
||||
|
|
@ -102,10 +115,31 @@ export class TaskLinkDetectionService {
|
|||
return result;
|
||||
}
|
||||
|
||||
private resolveDisplayText(
|
||||
parsed: ParsedTaskLink,
|
||||
linkType: "wikilink" | "markdown",
|
||||
taskInfo: TaskInfo,
|
||||
linkPath: string,
|
||||
parsedDisplayText?: string
|
||||
): string | undefined {
|
||||
if (linkType === "markdown") {
|
||||
if (parsedDisplayText) {
|
||||
return resolveTaskLinkDisplayText(parsedDisplayText, taskInfo.path, linkPath);
|
||||
}
|
||||
return formatTaskLinkSubpathDisplayText(taskInfo.title, parsed.subpath);
|
||||
}
|
||||
|
||||
if (parsed.hasExplicitAlias) {
|
||||
return parsedDisplayText;
|
||||
}
|
||||
|
||||
return formatTaskLinkSubpathDisplayText(taskInfo.title, parsed.subpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse wikilink syntax to extract link path and display text
|
||||
*/
|
||||
private parseWikilink(wikilinkText: string): { linkPath: string; displayText?: string } | null {
|
||||
private parseWikilink(wikilinkText: string): ParsedTaskLink | null {
|
||||
// Remove the [[ and ]] brackets
|
||||
const content = wikilinkText.slice(2, -2).trim();
|
||||
if (!content) return null;
|
||||
|
|
@ -123,6 +157,8 @@ export class TaskLinkDetectionService {
|
|||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: aliasPart,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +166,8 @@ export class TaskLinkDetectionService {
|
|||
const parsed = parseLinktext(content);
|
||||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: parsed.subpath || undefined,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +176,7 @@ export class TaskLinkDetectionService {
|
|||
*/
|
||||
private parseMarkdownLink(
|
||||
markdownLinkText: string
|
||||
): { linkPath: string; displayText?: string } | null {
|
||||
): ParsedTaskLink | null {
|
||||
// Parse markdown link: [text](path)
|
||||
const match = markdownLinkText.match(/^\[([^\]]*)\]\(([^)]+)\)$/);
|
||||
if (!match) return null;
|
||||
|
|
@ -172,7 +209,9 @@ export class TaskLinkDetectionService {
|
|||
|
||||
return {
|
||||
linkPath: parsed.path,
|
||||
displayText: displayText || parsed.subpath || undefined,
|
||||
displayText: displayText || undefined,
|
||||
subpath: parsed.subpath || undefined,
|
||||
hasExplicitAlias: Boolean(displayText),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
128
tests/unit/issues/issue-2072-heading-task-link-display.test.ts
Normal file
128
tests/unit/issues/issue-2072-heading-task-link-display.test.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { TFile } from "obsidian";
|
||||
import { buildTaskLinkDecorations } from "../../../src/editor/TaskLinkOverlay";
|
||||
import { TaskLinkWidget } from "../../../src/editor/TaskLinkWidget";
|
||||
import { TaskLinkDetectionService } from "../../../src/services/TaskLinkDetectionService";
|
||||
import { PluginFactory, TaskFactory } from "../../helpers/mock-factories";
|
||||
import type TaskNotesPlugin from "../../../src/main";
|
||||
import type { TaskInfo } from "../../../src/types";
|
||||
|
||||
jest.mock("../../../src/editor/TaskLinkWidget");
|
||||
|
||||
const MockTaskLinkWidget = TaskLinkWidget as jest.MockedClass<typeof TaskLinkWidget>;
|
||||
|
||||
describe("Issue #2072: heading task links keep the task name visible", () => {
|
||||
let plugin: TaskNotesPlugin;
|
||||
let task: TaskInfo;
|
||||
let activeWidgets: Map<string, TaskLinkWidget>;
|
||||
let displayTexts: Array<string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
displayTexts = [];
|
||||
|
||||
task = TaskFactory.createTask({
|
||||
path: "Tasks/Buy groceries.md",
|
||||
title: "Buy groceries",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
plugin = PluginFactory.createMockPlugin({
|
||||
settings: {
|
||||
enableTaskLinkOverlay: true,
|
||||
},
|
||||
app: {
|
||||
workspace: {
|
||||
getActiveViewOfType: jest.fn().mockReturnValue({
|
||||
file: {
|
||||
path: "Notes/Daily.md",
|
||||
},
|
||||
}),
|
||||
},
|
||||
metadataCache: {
|
||||
getFirstLinkpathDest: jest.fn().mockImplementation((linkPath: string) => {
|
||||
if (linkPath === "Buy groceries") {
|
||||
return { path: task.path };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.fn().mockImplementation((path: string) => {
|
||||
if (path === task.path) {
|
||||
const file = new TFile(path);
|
||||
Object.defineProperty(file, "stat", {
|
||||
value: { mtime: 1 },
|
||||
configurable: true,
|
||||
});
|
||||
return file;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
},
|
||||
cacheManager: {
|
||||
...PluginFactory.createMockPlugin().cacheManager,
|
||||
getCachedTaskInfoSync: jest.fn().mockImplementation((path: string) => {
|
||||
return path === task.path ? task : null;
|
||||
}),
|
||||
getTaskInfo: jest.fn().mockImplementation(async (path: string) => {
|
||||
return path === task.path ? task : null;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
activeWidgets = new Map();
|
||||
|
||||
MockTaskLinkWidget.mockImplementation((taskInfo, mockPlugin, originalText, displayText) => {
|
||||
displayTexts.push(displayText);
|
||||
return {
|
||||
toDOM: jest.fn(),
|
||||
eq: jest.fn().mockReturnValue(false),
|
||||
taskInfo,
|
||||
plugin: mockPlugin,
|
||||
originalText,
|
||||
displayText,
|
||||
} as unknown as TaskLinkWidget;
|
||||
});
|
||||
});
|
||||
|
||||
it("labels a no-alias heading wikilink with task title and heading", () => {
|
||||
const state = EditorState.create({
|
||||
doc: "link to heading: [[Buy groceries#sample heading]]",
|
||||
selection: EditorSelection.single(0),
|
||||
});
|
||||
|
||||
const decorations = buildTaskLinkDecorations(state, plugin, activeWidgets);
|
||||
|
||||
expect(decorations.size).toBe(1);
|
||||
expect(displayTexts).toEqual(["Buy groceries > sample heading"]);
|
||||
});
|
||||
|
||||
it("continues to honor explicit aliases on heading wikilinks", () => {
|
||||
const state = EditorState.create({
|
||||
doc: "link to heading: [[Buy groceries#sample heading|shopping note]]",
|
||||
selection: EditorSelection.single(0),
|
||||
});
|
||||
|
||||
const decorations = buildTaskLinkDecorations(state, plugin, activeWidgets);
|
||||
|
||||
expect(decorations.size).toBe(1);
|
||||
expect(displayTexts).toEqual(["shopping note"]);
|
||||
});
|
||||
|
||||
it("returns the same label through the async detection service", async () => {
|
||||
const service = new TaskLinkDetectionService(plugin);
|
||||
|
||||
const result = await service.detectTaskLink(
|
||||
"[[Buy groceries#sample heading]]",
|
||||
"Notes/Daily.md"
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
isValidTaskLink: true,
|
||||
taskPath: task.path,
|
||||
displayText: "Buy groceries > sample heading",
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue