feat: update tag workflow
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,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) ??
|
||||
|
||||
@@ -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(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user