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
|
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
|
## Dashboard
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -137,8 +137,8 @@ tags:
|
|||||||
- frontend
|
- frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Если `config.yml` есть, теги задачи вне каталога — warning. Hover на `tags` →
|
Если `config.yml` есть, теги вне каталога — warning (Quick Fix: **Add tag "…"
|
||||||
**Edit tags…** / **Add tag to project**.
|
to project**). Hover на `tags` → **Edit tags…**.
|
||||||
|
|
||||||
## Dashboard
|
## Dashboard
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
|||||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||||
import { NodeFileSystem } from "@/storage/NodeFileSystem";
|
import { NodeFileSystem } from "@/storage/NodeFileSystem";
|
||||||
import { VscodeConfigProvider } from "@/storage/VscodeConfigProvider";
|
import { VscodeConfigProvider } from "@/storage/VscodeConfigProvider";
|
||||||
|
import { registerTaskCodeActions } from "@/ui/taskCodeActions";
|
||||||
import { registerTaskCommands } from "@/ui/taskCommands";
|
import { registerTaskCommands } from "@/ui/taskCommands";
|
||||||
import { registerTaskDecorations } from "@/ui/taskDecorations";
|
import { registerTaskDecorations } from "@/ui/taskDecorations";
|
||||||
import { registerTaskDiagnostics } from "@/ui/taskDiagnostics";
|
import { registerTaskDiagnostics } from "@/ui/taskDiagnostics";
|
||||||
@@ -50,6 +51,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
registerTaskDiagnostics(context, { fs: fileSystem, config });
|
registerTaskDiagnostics(context, { fs: fileSystem, config });
|
||||||
registerTaskDecorations(context);
|
registerTaskDecorations(context);
|
||||||
|
registerTaskCodeActions(context);
|
||||||
|
|
||||||
registerTaskCommands(context, {
|
registerTaskCommands(context, {
|
||||||
repo,
|
repo,
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ title: [unterminated
|
|||||||
);
|
);
|
||||||
expect(unknown).toHaveLength(1);
|
expect(unknown).toHaveLength(1);
|
||||||
expect(unknown[0].message).toMatch(/nope/i);
|
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", () => {
|
it("does not warn when all tags are known", () => {
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ import {
|
|||||||
|
|
||||||
export type FrontmatterIssueSeverity = "error" | "warning";
|
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 = {
|
export type FrontmatterIssue = {
|
||||||
field?: string;
|
field?: string;
|
||||||
|
/** Stable issue code (e.g. unknown-tag). */
|
||||||
|
code?: string;
|
||||||
|
/** For unknown-tag: the tag string. */
|
||||||
|
tag?: string;
|
||||||
message: string;
|
message: string;
|
||||||
severity: FrontmatterIssueSeverity;
|
severity: FrontmatterIssueSeverity;
|
||||||
range: TextRange;
|
range: TextRange;
|
||||||
@@ -254,7 +261,9 @@ export function validateFrontmatter(
|
|||||||
if (!known.has(tag)) {
|
if (!known.has(tag)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
field: "tags",
|
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",
|
severity: "warning",
|
||||||
range:
|
range:
|
||||||
findTagTokenRange(content, tag) ??
|
findTagTokenRange(content, tag) ??
|
||||||
|
|||||||
@@ -343,18 +343,18 @@ export async function runEditTaskTags(
|
|||||||
catalogSet.add(tag);
|
catalogSet.add(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = await applyTags(document, selected);
|
await applyTags(document, selected);
|
||||||
if (ok) {
|
|
||||||
void vscode.window.showInformationMessage(
|
|
||||||
selected.length ? `Tags: ${selected.join(", ")}` : "Tags cleared",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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(
|
export async function runAddTagToProject(
|
||||||
deps: FrontmatterEditDeps,
|
deps: FrontmatterEditDeps,
|
||||||
uriArg?: string,
|
uriArg?: string,
|
||||||
|
tagArg?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const document = await resolveDocument(uriArg);
|
const document = await resolveDocument(uriArg);
|
||||||
if (!document) {
|
if (!document) {
|
||||||
@@ -372,6 +372,18 @@ export async function runAddTagToProject(
|
|||||||
return;
|
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);
|
const loaded = await loadProjectConfig(deps.fs, folder);
|
||||||
if (loaded.isErr()) {
|
if (loaded.isErr()) {
|
||||||
presentError(loaded.error);
|
presentError(loaded.error);
|
||||||
@@ -383,9 +395,6 @@ export async function runAddTagToProject(
|
|||||||
const unknown = current.filter((tag) => !known.has(tag));
|
const unknown = current.filter((tag) => !known.has(tag));
|
||||||
|
|
||||||
if (unknown.length === 0) {
|
if (unknown.length === 0) {
|
||||||
void vscode.window.showInformationMessage(
|
|
||||||
"All task tags are already in project config",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,8 +421,4 @@ export async function runAddTagToProject(
|
|||||||
return;
|
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(
|
vscode.commands.registerCommand(
|
||||||
"xboctukFlow.addTagToProject",
|
"xboctukFlow.addTagToProject",
|
||||||
(uri?: string) =>
|
(uri?: string, tag?: string) =>
|
||||||
runAddTagToProject({ fs: deps.fs, config: deps.config }, uri),
|
runAddTagToProject(
|
||||||
|
{ fs: deps.fs, config: deps.config },
|
||||||
|
uri,
|
||||||
|
tag,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
vscode.commands.registerCommand("xboctukFlow.openDashboard", () =>
|
vscode.commands.registerCommand("xboctukFlow.openDashboard", () =>
|
||||||
runOpenDashboard(deps),
|
runOpenDashboard(deps),
|
||||||
|
|||||||
@@ -45,10 +45,7 @@ function hoverAssigneeMe(document: vscode.TextDocument): vscode.MarkdownString {
|
|||||||
function hoverTags(document: vscode.TextDocument): vscode.MarkdownString {
|
function hoverTags(document: vscode.TextDocument): vscode.MarkdownString {
|
||||||
const uri = document.uri.toString();
|
const uri = document.uri.toString();
|
||||||
const md = new vscode.MarkdownString(
|
const md = new vscode.MarkdownString(
|
||||||
[
|
`[$(edit) Edit tags…](${commandUri("xboctukFlow.editTaskTags", [uri])})`,
|
||||||
`[$(edit) Edit tags…](${commandUri("xboctukFlow.editTaskTags", [uri])})`,
|
|
||||||
`[$(repo-push) Add tag to project](${commandUri("xboctukFlow.addTagToProject", [uri])})`,
|
|
||||||
].join(" · "),
|
|
||||||
);
|
);
|
||||||
md.isTrusted = true;
|
md.isTrusted = true;
|
||||||
md.supportThemeIcons = true;
|
md.supportThemeIcons = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as vscode from "vscode";
|
|||||||
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
|
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
|
||||||
import { loadProjectConfig } from "@/commands/loadProjectConfig";
|
import { loadProjectConfig } from "@/commands/loadProjectConfig";
|
||||||
import {
|
import {
|
||||||
|
UNKNOWN_TAG_ISSUE_CODE,
|
||||||
validateFrontmatter,
|
validateFrontmatter,
|
||||||
type FrontmatterIssue,
|
type FrontmatterIssue,
|
||||||
} from "@/frontmatter/validateFrontmatter";
|
} from "@/frontmatter/validateFrontmatter";
|
||||||
@@ -12,6 +13,27 @@ import { resolveTaskFolderForPath } from "./resolveTaskFolder";
|
|||||||
|
|
||||||
const DEBOUNCE_MS = 200;
|
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 = {
|
export type TaskDiagnosticsDeps = {
|
||||||
fs: IFileSystem;
|
fs: IFileSystem;
|
||||||
config: IConfigProvider;
|
config: IConfigProvider;
|
||||||
@@ -47,7 +69,9 @@ function toDiagnostic(issue: FrontmatterIssue): vscode.Diagnostic {
|
|||||||
severity,
|
severity,
|
||||||
);
|
);
|
||||||
diagnostic.source = "xboctuk-flow";
|
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;
|
diagnostic.code = issue.field;
|
||||||
}
|
}
|
||||||
return diagnostic;
|
return diagnostic;
|
||||||
|
|||||||
Reference in New Issue
Block a user