feat: update tag workflow

This commit is contained in:
2026-07-18 06:57:52 +05:00
parent 6eedfb2bc5
commit 9b1281ce49
10 changed files with 134 additions and 25 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ tags:
```
If `config.yml` exists, tags on a task that are **not** in the catalog show as
warnings. Hover `tags`**Edit tags…** / **Add tag to project**.
warnings (Quick Fix: **Add tag "…" to project**). Hover `tags`**Edit tags…**.
## Dashboard
+2 -2
View File
@@ -137,8 +137,8 @@ tags:
- frontend
```
Если `config.yml` есть, теги задачи вне каталога — warning. Hover на `tags`
**Edit tags…** / **Add tag to project**.
Если `config.yml` есть, теги вне каталога — warning (Quick Fix: **Add tag "…"
to project**). Hover на `tags`**Edit tags…**.
## Dashboard
+2
View File
@@ -5,6 +5,7 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { NodeFileSystem } from "@/storage/NodeFileSystem";
import { VscodeConfigProvider } from "@/storage/VscodeConfigProvider";
import { registerTaskCodeActions } from "@/ui/taskCodeActions";
import { registerTaskCommands } from "@/ui/taskCommands";
import { registerTaskDecorations } from "@/ui/taskDecorations";
import { registerTaskDiagnostics } from "@/ui/taskDiagnostics";
@@ -50,6 +51,7 @@ export function activate(context: vscode.ExtensionContext) {
registerTaskDiagnostics(context, { fs: fileSystem, config });
registerTaskDecorations(context);
registerTaskCodeActions(context);
registerTaskCommands(context, {
repo,
@@ -107,6 +107,8 @@ title: [unterminated
);
expect(unknown).toHaveLength(1);
expect(unknown[0].message).toMatch(/nope/i);
expect(unknown[0].code).toBe("unknown-tag");
expect(unknown[0].tag).toBe("nope");
});
it("does not warn when all tags are known", () => {
+10 -1
View File
@@ -10,8 +10,15 @@ import {
export type FrontmatterIssueSeverity = "error" | "warning";
/** Diagnostic / Code Action id for a tag not present in project config. */
export const UNKNOWN_TAG_ISSUE_CODE = "unknown-tag";
export type FrontmatterIssue = {
field?: string;
/** Stable issue code (e.g. unknown-tag). */
code?: string;
/** For unknown-tag: the tag string. */
tag?: string;
message: string;
severity: FrontmatterIssueSeverity;
range: TextRange;
@@ -254,7 +261,9 @@ export function validateFrontmatter(
if (!known.has(tag)) {
issues.push({
field: "tags",
message: `Unknown tag "${tag}"; add it to project config or pick from catalog`,
code: UNKNOWN_TAG_ISSUE_CODE,
tag,
message: `Unknown tag "${tag}"; add it to project config`,
severity: "warning",
range:
findTagTokenRange(content, tag) ??
+19 -14
View File
@@ -343,18 +343,18 @@ export async function runEditTaskTags(
catalogSet.add(tag);
}
const ok = await applyTags(document, selected);
if (ok) {
void vscode.window.showInformationMessage(
selected.length ? `Tags: ${selected.join(", ")}` : "Tags cleared",
);
}
await applyTags(document, selected);
}
/** Add unknown task tags into the task-folder config.yml. */
/**
* Add tag(s) to task-folder config.yml.
* Prefer `tagArg` from Code Action on an unknown-tag warning.
* Without tag: multi-select remaining unknown tags from the task.
*/
export async function runAddTagToProject(
deps: FrontmatterEditDeps,
uriArg?: string,
tagArg?: string,
): Promise<void> {
const document = await resolveDocument(uriArg);
if (!document) {
@@ -372,6 +372,18 @@ export async function runAddTagToProject(
return;
}
const single = normalizeTag(tagArg ?? "");
if (single) {
const result = await addProjectTag(deps.fs, {
folderPath: folder,
tag: single,
});
if (result.isErr()) {
presentError(result.error);
}
return;
}
const loaded = await loadProjectConfig(deps.fs, folder);
if (loaded.isErr()) {
presentError(loaded.error);
@@ -383,9 +395,6 @@ export async function runAddTagToProject(
const unknown = current.filter((tag) => !known.has(tag));
if (unknown.length === 0) {
void vscode.window.showInformationMessage(
"All task tags are already in project config",
);
return;
}
@@ -412,8 +421,4 @@ export async function runAddTagToProject(
return;
}
}
void vscode.window.showInformationMessage(
`Added to project config: ${picked.map((p) => p.tag).join(", ")}`,
);
}
+66
View File
@@ -0,0 +1,66 @@
import * as vscode from "vscode";
import { isTaskFileName, TASK_LANGUAGE_ID } from "@/model/taskFile";
import { parseUnknownTagFromDiagnosticCode } from "./taskDiagnostics";
/**
* Quick Fix on unknown-tag warnings: «Add tag "…" to project».
*/
export class TaskCodeActionProvider implements vscode.CodeActionProvider {
static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix];
provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
): vscode.CodeAction[] {
if (
document.languageId !== TASK_LANGUAGE_ID &&
!isTaskFileName(document.fileName)
) {
return [];
}
const actions: vscode.CodeAction[] = [];
const uri = document.uri.toString();
for (const diagnostic of context.diagnostics) {
if (diagnostic.source !== "xboctuk-flow") continue;
const tag = parseUnknownTagFromDiagnosticCode(diagnostic.code);
if (!tag) continue;
const action = new vscode.CodeAction(
`Add tag "${tag}" to project`,
vscode.CodeActionKind.QuickFix,
);
action.diagnostics = [diagnostic];
action.isPreferred = true;
action.command = {
command: "xboctukFlow.addTagToProject",
title: `Add tag "${tag}" to project`,
arguments: [uri, tag],
};
actions.push(action);
}
return actions;
}
}
export function registerTaskCodeActions(
context: vscode.ExtensionContext,
): void {
const selector: vscode.DocumentSelector = [
{ language: TASK_LANGUAGE_ID },
{ pattern: "**/*.xflow.md" },
];
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
selector,
new TaskCodeActionProvider(),
{
providedCodeActionKinds:
TaskCodeActionProvider.providedCodeActionKinds,
},
),
);
}
+6 -2
View File
@@ -267,8 +267,12 @@ export function registerTaskCommands(
),
vscode.commands.registerCommand(
"xboctukFlow.addTagToProject",
(uri?: string) =>
runAddTagToProject({ fs: deps.fs, config: deps.config }, uri),
(uri?: string, tag?: string) =>
runAddTagToProject(
{ fs: deps.fs, config: deps.config },
uri,
tag,
),
),
vscode.commands.registerCommand("xboctukFlow.openDashboard", () =>
runOpenDashboard(deps),
+1 -4
View File
@@ -45,10 +45,7 @@ function hoverAssigneeMe(document: vscode.TextDocument): vscode.MarkdownString {
function hoverTags(document: vscode.TextDocument): vscode.MarkdownString {
const uri = document.uri.toString();
const md = new vscode.MarkdownString(
[
`[$(edit) Edit tags…](${commandUri("xboctukFlow.editTaskTags", [uri])})`,
`[$(repo-push) Add tag to project](${commandUri("xboctukFlow.addTagToProject", [uri])})`,
].join(" · "),
`[$(edit) Edit tags…](${commandUri("xboctukFlow.editTaskTags", [uri])})`,
);
md.isTrusted = true;
md.supportThemeIcons = true;
+25 -1
View File
@@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
import { loadProjectConfig } from "@/commands/loadProjectConfig";
import {
UNKNOWN_TAG_ISSUE_CODE,
validateFrontmatter,
type FrontmatterIssue,
} from "@/frontmatter/validateFrontmatter";
@@ -12,6 +13,27 @@ import { resolveTaskFolderForPath } from "./resolveTaskFolder";
const DEBOUNCE_MS = 200;
/** Encode unknown-tag diagnostic code so Code Actions can recover the tag. */
export function unknownTagDiagnosticCode(tag: string): string {
return `${UNKNOWN_TAG_ISSUE_CODE}:${tag}`;
}
export function parseUnknownTagFromDiagnosticCode(
code: string | number | { value: string | number; target: vscode.Uri } | undefined,
): string | undefined {
const raw =
typeof code === "string"
? code
: typeof code === "object" && code !== null && "value" in code
? String(code.value)
: undefined;
if (!raw || !raw.startsWith(`${UNKNOWN_TAG_ISSUE_CODE}:`)) {
return undefined;
}
const tag = raw.slice(UNKNOWN_TAG_ISSUE_CODE.length + 1);
return tag.length > 0 ? tag : undefined;
}
export type TaskDiagnosticsDeps = {
fs: IFileSystem;
config: IConfigProvider;
@@ -47,7 +69,9 @@ function toDiagnostic(issue: FrontmatterIssue): vscode.Diagnostic {
severity,
);
diagnostic.source = "xboctuk-flow";
if (issue.field) {
if (issue.code === UNKNOWN_TAG_ISSUE_CODE && issue.tag) {
diagnostic.code = unknownTagDiagnosticCode(issue.tag);
} else if (issue.field) {
diagnostic.code = issue.field;
}
return diagnostic;