From fbdbdcedeb0d657a0626b4530c67eea5053dbb4b Mon Sep 17 00:00:00 2001 From: Juan Miguel Yera Date: Mon, 9 Feb 2026 13:23:04 +0100 Subject: [PATCH] feat(tree): sort tasks by Taskfile definition order When the sort mode is "default", tasks and namespaces in the sidebar are now ordered by their location.line property, preserving the original definition order from the Taskfile. Namespaces are positioned by the smallest line number of their contained tasks, scoped to the parent Taskfile to prevent included files from altering the order. Co-Authored-By: Claude --- .gitignore | 1 + src/providers/taskTreeDataProvider.ts | 93 ++++++++++++++++++++------- src/services/taskfile.ts | 6 +- tsconfig.json | 1 + 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 03661b3..1c7db28 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.vsix .task test-workspace/.vscode +pnpm-workspace.yaml diff --git a/src/providers/taskTreeDataProvider.ts b/src/providers/taskTreeDataProvider.ts index c4ab2ab..ecf21f6 100644 --- a/src/providers/taskTreeDataProvider.ts +++ b/src/providers/taskTreeDataProvider.ts @@ -1,10 +1,39 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { TreeItem, TaskTreeItem, NamespaceTreeItem, WorkspaceTreeItem } from '../elements/treeItem.js'; +import { NamespaceTreeItem, TaskTreeItem, TreeItem, WorkspaceTreeItem } from '../elements/treeItem.js'; import { Namespace, Task } from '../models/models.js'; const namespaceSeparator = ':'; +/** + * Recursively finds the smallest location.line across all tasks + * in a namespace and its sub-namespaces. When taskfile is provided, + * only tasks belonging to that file are considered (this prevents + * included files from pulling a namespace out of order). + * Returns Infinity if no matching tasks exist. + */ +function getMinLine(namespace: Namespace, taskfile?: string): number { + let min = Infinity; + if (namespace.tasks) { + for (const task of namespace.tasks) { + if (task.location && task.location.line < min) { + if (!taskfile || task.location.taskfile === taskfile) { + min = task.location.line; + } + } + } + } + if (namespace.namespaces) { + for (const [, child] of Object.entries(namespace.namespaces)) { + const childMin = getMinLine(child, taskfile); + if (childMin < min) { + min = childMin; + } + } + } + return min; +} + export class TaskTreeDataProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; @@ -46,7 +75,8 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider { return Promise.resolve(this.createTreeItems( this._namespaces[0].workspace ?? "", this._namespaces[0].namespaces, - this._namespaces[0].tasks + this._namespaces[0].tasks, + this._namespaces[0].location )); } @@ -55,7 +85,8 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider { return Promise.resolve(this.createTreeItems( parent.workspace, parent.namespace.namespaces, - parent.namespace.tasks + parent.namespace.tasks, + parent.namespace.location )); } @@ -65,40 +96,52 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider { createTreeItems( workspace: string, namespaces: Map, - tasks: Task[] + tasks: Task[], + taskfile?: string ): TreeItem[] { - var treeItems: TreeItem[] = []; + // Build a unified list with sort keys so we can interleave + // namespaces and tasks in their original Taskfile order. + const entries: { line: number; item: TreeItem }[] = []; - // Add each namespace to the tree + // Add each namespace to the list if (namespaces) { for (const [key, namespace] of Object.entries(namespaces)){ - treeItems = treeItems.concat(new NamespaceTreeItem( - key, - workspace, - namespace, - vscode.TreeItemCollapsibleState.Collapsed - )); + entries.push({ + line: getMinLine(namespace, taskfile), + item: new NamespaceTreeItem( + key, + workspace, + namespace, + vscode.TreeItemCollapsibleState.Collapsed + ) + }); } } - // Add each task to the tree + // Add each task to the list if (tasks) { for (const task of tasks) { - treeItems = treeItems.concat(new TaskTreeItem( - this._nesting ? task.name.split(namespaceSeparator).pop() ?? task.name : task.name, - workspace, - task, - vscode.TreeItemCollapsibleState.None, - { - command: 'vscode-task.goToDefinition', - title: 'Go to Definition', - arguments: [task, true] - } - )); + entries.push({ + line: task.location?.line ?? Infinity, + item: new TaskTreeItem( + this._nesting ? task.name.split(namespaceSeparator).pop() ?? task.name : task.name, + workspace, + task, + vscode.TreeItemCollapsibleState.None, + { + command: 'vscode-task.goToDefinition', + title: 'Go to Definition', + arguments: [task, true] + } + ) + }); } } - return treeItems; + // Sort by line number to preserve original Taskfile order + entries.sort((a, b) => a.line - b.line); + + return entries.map((e) => e.item); } getWorkspaces(): WorkspaceTreeItem[] { diff --git a/src/services/taskfile.ts b/src/services/taskfile.ts index cc2faf1..7f7c9e6 100644 --- a/src/services/taskfile.ts +++ b/src/services/taskfile.ts @@ -1,14 +1,14 @@ import { Endpoints } from "@octokit/types"; import * as cp from 'child_process'; import * as fs from 'fs'; +import { stripVTControlCharacters } from 'node:util'; import { Octokit } from 'octokit'; import * as path from 'path'; import * as semver from 'semver'; import * as vscode from 'vscode'; import { Namespace, Task } from '../models/models.js'; -import { OutputTo, TerminalClose, TerminalPer, TreeSort, settings } from '../utils/settings.js'; import { log } from '../utils/log.js'; -import { stripVTControlCharacters} from 'node:util'; +import { OutputTo, TerminalClose, TerminalPer, TreeSort, settings } from '../utils/settings.js'; const octokit = new Octokit(); type ReleaseRequest = Endpoints["GET /repos/{owner}/{repo}/releases/latest"]["parameters"]; @@ -132,7 +132,7 @@ class TaskfileService { owner: 'go-task', repo: 'task' }; - let response: ReleaseResponse = await octokit.rest.repos.getLatestRelease(request); + let response = await octokit.rest.repos.getLatestRelease(request); return Promise.resolve(semver.parse(response.data.tag_name)); } diff --git a/tsconfig.json b/tsconfig.json index 0276fe1..1f3ef30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,6 @@ "sourceMap": true, "rootDir": "src", "strict": true, + "types": ["node", "mocha"] } }