Skip to content

Commit 5ebf99f

Browse files
committed
feat: add support for task templates
1 parent 5471613 commit 5ebf99f

20 files changed

Lines changed: 1327 additions & 100 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ To enable auto-configuration, set [`mise.configureExtensionsAutomatically`](http
5858

5959
**Important:** By default, tools from your global mise configuration (`~/.config/mise/config.toml`) are included when auto-configuring other VS Code extensions ([`mise.configureExtensionsIncludeGlobalTools`](https://hverlin.github.io/mise-vscode/reference/settings/#miseconfigureextensionsincludeglobaltools) is `true` for backward compatibility). You can set it to `false` to keep your `.vscode/settings.json` clean and ensure extensions are only configured for tools actually used in your project.
6060

61-
## Features
61+
## Features
6262

6363
The mise-vscode extension integrates mise's core functionality into VS Code, helping you manage your development environment directly from the editor. You can handle task running, tool versions, and environment variables through a simple interface. Here's what's available:
6464

@@ -80,6 +80,8 @@ The mise-vscode extension integrates mise's core functionality into VS Code, hel
8080
- 📝 View task definitions
8181
- ➕ Create new toml & file tasks
8282
- ⚡ Autocompletion of task dependencies
83+
- 🧬 Autocompletion, hover, go to definition and find references for
84+
[task templates](https://mise.jdx.dev/tasks/templates.html) (`extends`)
8385
- 🕸️ View graph of task dependencies
8486

8587
### [Tool Management](https://hverlin.github.io/mise-vscode/reference/tools/)

bun-preload-test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ mock.module("vscode", () => {
1515
) {}
1616
}
1717

18+
class MarkdownString {
19+
public supportHtml = false;
20+
constructor(public value = "") {}
21+
22+
appendMarkdown(markdown: string) {
23+
this.value += markdown;
24+
return this;
25+
}
26+
27+
appendText(text: string) {
28+
this.value += text;
29+
return this;
30+
}
31+
32+
appendCodeblock(code: string, language = "") {
33+
this.value += `\n\`\`\`${language}\n${code}\n\`\`\`\n`;
34+
return this;
35+
}
36+
}
37+
1838
return {
1939
workspace: {},
2040
window: {
@@ -25,7 +45,16 @@ mock.module("vscode", () => {
2545
createOutputChannel: () => {},
2646
},
2747
ConfigurationTarget: {},
48+
MarkdownString,
2849
Position,
2950
Range: Range,
51+
Uri: {
52+
file: (fsPath: string) => ({
53+
fsPath,
54+
path: fsPath,
55+
scheme: "file",
56+
toString: () => `file://${fsPath}`,
57+
}),
58+
},
3059
};
3160
});

docs/src/content/docs/reference/Tasks.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,41 @@ context menu in the activity bar:
146146
This requires mise `2026.8.1` or later. Nothing is shown for tasks that do not
147147
enable the cache.
148148

149+
## Task templates
150+
151+
[Task templates](https://mise.jdx.dev/tasks/templates.html) are reusable task
152+
definitions declared in a `[task_templates.<name>]` section. A task picks one up
153+
with `extends = "<name>"`, and can override any of its fields:
154+
155+
```toml
156+
[task_templates."python:build"]
157+
description = "Build a python package"
158+
run = "uv build"
159+
tools = { python = "3.12", uv = "latest" }
160+
161+
[tasks.build]
162+
extends = "python:build"
163+
run = "uv build --wheel" # overrides the template command
164+
```
165+
166+
The extension resolves templates the way mise does, from the config file
167+
declaring the task and from its parent config files, which makes them
168+
particularly useful in a [monorepo](#monorepo-tasks). It provides:
169+
170+
- **Autocompletion** of the template names in `extends = "<name>"`, showing the
171+
declared fields and which config file each template comes from
172+
- **Hover** on `extends = "<name>"`, showing the template it resolves to, and on
173+
a `[task_templates.<name>]` declaration, showing how many tasks extend it
174+
- **Go to definition** from `extends = "<name>"` to the `[task_templates.<name>]`
175+
entry, in the same file or in a parent config file
176+
- **Find references** from a template declaration to every task extending it,
177+
anywhere in the workspace
178+
- Task templates in the outline view, and `task_template` / `task_extends`
179+
snippets
180+
181+
Task hovers show the template a task extends. The fields shown for the task
182+
itself are the ones mise resolved, i.e. after the template has been merged in.
183+
149184
## Monorepo tasks
150185

151186
The extension supports
@@ -173,8 +208,8 @@ Navigation understands all the ways tasks can reference each other:
173208
project depends on, following the workspace projects graph
174209
- task aliases (`alias = "fmt"`), including in other projects
175210

176-
Tasks declared with `extends = "<name>"` support go-to-definition to the
177-
`[task_templates.<name>]` entry, in the same file or a parent config file.
211+
[Task templates](#task-templates) declared in the monorepo root config are
212+
resolved for the tasks of every project extending them.
178213
Dependencies added by `[monorepo.task_defaults]` in the root config are shown
179214
in the task tooltips and included when searching for task references.
180215

docs/src/content/docs/reference/mise.toml-language-support.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ syntax highlighting even though they have no `.toml` extension.
6868

6969
Code completion is provided for `depends = ["task_name"]`, `depends_post = ["task_name"]`, `wait_for = ["task_name"]`.
7070

71+
`extends = "template_name"` completes the
72+
[task templates](https://mise.jdx.dev/tasks/templates.html) declared in the file
73+
being edited and in its parent config files. See the
74+
[task templates section](/reference/tasks/#task-templates) of the tasks
75+
reference for the hover, go-to-definition, and find-references support.
76+
7177
### Task arguments (usage spec)
7278

7379
Task arguments are declared with the
@@ -120,6 +126,9 @@ This extension adds the following code lens features:
120126

121127
- Cmd/Ctrl+Click on an included file will open that file (example:
122128
`include = ["tasks.toml"`])
129+
- Cmd/Ctrl+Click on `extends = "template_name"` opens the
130+
`[task_templates.template_name]` declaration; _Find all references_ on a
131+
declaration lists every task extending it
123132

124133
### Syntax highlighting for shebang
125134

snippets/toml-tasks-snippets.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
"description": "Create a simple mise task",
2626
"body": ["[tasks.${1:taskname}]", "run = \"echo 'running...'\"", "$0"]
2727
},
28+
"tomlTaskTemplate": {
29+
"prefix": "task_template",
30+
"description": "Create a reusable task template that tasks can extend",
31+
"body": [
32+
"[task_templates.${1:my:template}]",
33+
"${2:description = '${3:Template description}'\n}${4:tools = { ${5:node} = \"${6:latest}\" }\n}run = \"echo 'running...'\"",
34+
"$0"
35+
]
36+
},
2837
"tomlTaskScript": {
2938
"prefix": "task_script",
3039
"description": "Create a mise task with multiline script",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
[tasks.build]
2+
# overrides the description and the run command of the root task template
3+
extends = "rust:build"
24
description = "Depends on a task of another crate"
35
depends = ["//crates/protocol:build"]
46
run = "echo 'Building agent crate'"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[tasks.build]
2-
description = "Build the protocol crate"
3-
run = "echo 'Building protocol crate'"
2+
# the description and the run command come from the root task template
3+
extends = "rust:build"

src/e2e-tests/fixtures/monorepo-workspace/mise.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ depends = ["^build"]
2222
description = "Show information about the project"
2323
run = "echo 'project info'"
2424

25+
# namespaced template name, extended by several crates
26+
[task_templates."rust:build"]
27+
description = "Build a rust crate"
28+
run = "echo 'Building crate'"
29+
2530
[settings]
2631
# package.json script tasks and `mise tasks graph` are experimental-gated
2732
experimental = true

src/e2e-tests/monorepo/monorepo-tasks.e2e.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,144 @@ suite("Monorepo Tasks Test Suite", function () {
427427
assert.equal(targetRange.start.line, expectedLine);
428428
});
429429

430+
test("Should complete task template names in extends", async () => {
431+
const uri = vscode.Uri.file(
432+
path.join(workspaceRoot, "crates", "protocol", "mise.toml"),
433+
);
434+
const document = await vscode.workspace.openTextDocument(uri);
435+
436+
const lineIndex = document
437+
.getText()
438+
.split("\n")
439+
.findIndex((line) => line.includes("extends ="));
440+
assert.ok(lineIndex >= 0, "extends line should be found in the fixture");
441+
// just after the opening quote, i.e. with nothing typed yet
442+
const character = document.lineAt(lineIndex).text.indexOf('"') + 1;
443+
444+
const completions =
445+
await vscode.commands.executeCommand<vscode.CompletionList>(
446+
"vscode.executeCompletionItemProvider",
447+
uri,
448+
new vscode.Position(lineIndex, character),
449+
);
450+
const labels = completions.items.map((item) =>
451+
typeof item.label === "string" ? item.label : item.label.label,
452+
);
453+
454+
// both templates of the root config are offered, not only the local one
455+
for (const expected of ["rust:build", "project-info"]) {
456+
assert.ok(
457+
labels.includes(expected),
458+
`expected ${expected} in ${JSON.stringify(labels)}`,
459+
);
460+
}
461+
});
462+
463+
test("Should show the resolved template when hovering extends", async () => {
464+
const uri = vscode.Uri.file(
465+
path.join(workspaceRoot, "crates", "protocol", "mise.toml"),
466+
);
467+
const document = await vscode.workspace.openTextDocument(uri);
468+
469+
const lineIndex = document
470+
.getText()
471+
.split("\n")
472+
.findIndex((line) => line.includes("extends ="));
473+
assert.ok(lineIndex >= 0, "extends line should be found in the fixture");
474+
const character = document.lineAt(lineIndex).text.indexOf("rust:build") + 1;
475+
476+
const hovers =
477+
(await vscode.commands.executeCommand<vscode.Hover[]>(
478+
"vscode.executeHoverProvider",
479+
uri,
480+
new vscode.Position(lineIndex, character),
481+
)) ?? [];
482+
const hoverText = hovers
483+
.flatMap((hover) => hover.contents)
484+
.map((content) =>
485+
typeof content === "string"
486+
? content
487+
: (content as { value: string }).value,
488+
)
489+
.join("\n");
490+
491+
assert.ok(
492+
hoverText.includes("Build a rust crate"),
493+
`the hover should describe the template, got ${hoverText}`,
494+
);
495+
assert.ok(
496+
hoverText.includes("echo 'Building crate'"),
497+
`the hover should show the template command, got ${hoverText}`,
498+
);
499+
// the template comes from the root config, not from this one
500+
assert.ok(
501+
hoverText.includes("mise.toml"),
502+
`the hover should link to the declaring config, got ${hoverText}`,
503+
);
504+
});
505+
506+
test("Should find every task extending a task template", async () => {
507+
const uri = vscode.Uri.file(path.join(workspaceRoot, "mise.toml"));
508+
const document = await vscode.workspace.openTextDocument(uri);
509+
510+
const headerLine = document
511+
.getText()
512+
.split("\n")
513+
.findIndex((line) => line.includes('[task_templates."rust:build"]'));
514+
assert.ok(headerLine >= 0, "the template header should be in the fixture");
515+
const character = document.lineAt(headerLine).text.indexOf("rust:build");
516+
517+
const locations = await vscode.commands.executeCommand<vscode.Location[]>(
518+
"vscode.executeReferenceProvider",
519+
uri,
520+
new vscode.Position(headerLine, character),
521+
);
522+
const locationPaths = locations.map((location) => location.uri.path);
523+
524+
for (const expected of [
525+
"crates/agent/mise.toml",
526+
"crates/protocol/mise.toml",
527+
]) {
528+
assert.ok(
529+
locationPaths.some((locationPath) => locationPath.endsWith(expected)),
530+
`expected an extends in ${expected}, got ${JSON.stringify(locationPaths)}`,
531+
);
532+
}
533+
});
534+
535+
test("Should report how many tasks extend a template on hover", async () => {
536+
const uri = vscode.Uri.file(path.join(workspaceRoot, "mise.toml"));
537+
const document = await vscode.workspace.openTextDocument(uri);
538+
539+
const headerLine = document
540+
.getText()
541+
.split("\n")
542+
.findIndex((line) => line.includes('[task_templates."rust:build"]'));
543+
assert.ok(headerLine >= 0, "the template header should be in the fixture");
544+
const character =
545+
document.lineAt(headerLine).text.indexOf("rust:build") + 1;
546+
547+
const hovers =
548+
(await vscode.commands.executeCommand<vscode.Hover[]>(
549+
"vscode.executeHoverProvider",
550+
uri,
551+
new vscode.Position(headerLine, character),
552+
)) ?? [];
553+
const hoverText = hovers
554+
.flatMap((hover) => hover.contents)
555+
.map((content) =>
556+
typeof content === "string"
557+
? content
558+
: (content as { value: string }).value,
559+
)
560+
.join("\n");
561+
562+
assert.ok(
563+
hoverText.includes("Extended by 2 tasks"),
564+
`the hover should count the extending tasks, got ${hoverText}`,
565+
);
566+
});
567+
430568
test("Should find dependent tasks across the monorepo", async () => {
431569
const uri = vscode.Uri.file(
432570
path.join(workspaceRoot, "projects", "frontend", "mise.toml"),

src/miseExtension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
import { TaskDefinitionProvider } from "./providers/TaskDefinitionProvider";
7070
import { TaskHoverProvider } from "./providers/TaskHoverProvider";
7171
import { TaskReferenceProvider } from "./providers/TaskReferenceProvider";
72+
import { TaskTemplateCompletionProvider } from "./providers/TaskTemplateCompletionProvider";
7273
import { ToolCompletionProvider } from "./providers/ToolCompletionProvider";
7374
import { registerTomlFileLinks } from "./providers/taskIncludesNavigation";
7475
import {
@@ -543,6 +544,11 @@ export class MiseExtension {
543544
new ConfigRootsCompletionProvider(),
544545
...['"', "'", "[", ",", "/"],
545546
),
547+
vscode.languages.registerCompletionItemProvider(
548+
allTomlFilesSelector,
549+
new TaskTemplateCompletionProvider(this.miseService),
550+
...['"', "'", "=", ":"],
551+
),
546552
vscode.languages.registerDefinitionProvider(
547553
allTomlFilesSelector,
548554
new TaskDefinitionProvider(this.miseService),

0 commit comments

Comments
 (0)