Skip to content

Commit a5416bc

Browse files
authored
fix(#89, #59): Fix Namespace Tree Mapping (#90)
1 parent c5ac2e9 commit a5416bc

File tree

6 files changed

+561
-508
lines changed

6 files changed

+561
-508
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@
321321
"@vscode/test-electron": "^2.2.3",
322322
"@vscode/vsce": "^2.19.0",
323323
"esbuild": "^0.17.18",
324-
"eslint": "^8.39.0",
324+
"eslint": "^8.2.0",
325325
"glob": "^8.1.0",
326326
"mocha": "^10.2.0",
327327
"ovsx": "^0.8.1",

src/elements/treeItem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class NamespaceTreeItem extends vscode.TreeItem {
2222
constructor(
2323
readonly label: string,
2424
readonly workspace: string,
25-
readonly namespace: string,
25+
readonly namespaceMap: any,
2626
readonly tasks: models.Task[],
2727
readonly collapsibleState: vscode.TreeItemCollapsibleState,
2828
readonly command?: vscode.Command

src/models/taskfile.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export type TaskMapping = {
2+
[key: string]: TaskMapping | null;
3+
};
4+
15
export interface Taskfile {
26
tasks: Task[];
37
location: string; // The location of the actual Taskfile

src/providers/taskTreeDataProvider.ts

Lines changed: 87 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import * as path from 'path';
12
import * as vscode from 'vscode';
2-
import * as models from '../models';
33
import * as elements from '../elements';
4-
import * as path from 'path';
4+
import * as models from '../models';
55

66
const namespaceSeparator = ':';
77

88
export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.TreeItem> {
99
private _onDidChangeTreeData: vscode.EventEmitter<elements.TaskTreeItem | undefined> = new vscode.EventEmitter<elements.TaskTreeItem | undefined>();
1010
readonly onDidChangeTreeData: vscode.Event<elements.TaskTreeItem | undefined> = this._onDidChangeTreeData.event;
1111
private _taskfiles?: models.Taskfile[];
12+
private _treeViewMap: models.TaskMapping = {};
1213

1314
constructor(
1415
private nestingEnabled: boolean = false
@@ -44,6 +45,7 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.Tr
4445

4546
var tasks: models.Task[] | undefined;
4647
var parentNamespace = "";
48+
var namespaceMap = this._treeViewMap;
4749
var workspace = "";
4850

4951
// If there is no parent and exactly one workspace folder or if the parent is a workspace
@@ -61,59 +63,61 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.Tr
6163
// If there is a parent and it is a namespace
6264
if (parent instanceof elements.NamespaceTreeItem) {
6365
tasks = parent.tasks;
64-
parentNamespace = parent.namespace;
66+
parentNamespace = parent.label;
67+
namespaceMap = parent.namespaceMap;
6568
workspace = parent.workspace;
6669
}
6770

68-
if (tasks) {
69-
let namespaceTreeItems = new Map<string, elements.NamespaceTreeItem>();
70-
let taskTreeItems: elements.TaskTreeItem[] = [];
71-
for (let task of tasks) {
7271

73-
let fullNamespacePath = getFullNamespacePath(task);
74-
let namespacePath = trimParentNamespace(fullNamespacePath, parentNamespace);
72+
if (tasks === undefined) {
73+
return Promise.resolve([]);
74+
}
75+
76+
let namespaceTreeItems = new Map<string, elements.NamespaceTreeItem>();
77+
let taskTreeItems: elements.TaskTreeItem[] = [];
78+
tasks.forEach(task => {
79+
let taskName = task.name.split(":").pop() ?? task.name;
80+
let namespacePath = trimParentNamespace(task.name, parentNamespace);
81+
let namespaceName = getNamespaceName(namespacePath);
82+
83+
if (taskName in namespaceMap) {
84+
let item = new elements.TaskTreeItem(
85+
task.name.split(namespaceSeparator).pop() ?? task.name,
86+
workspace,
87+
task,
88+
vscode.TreeItemCollapsibleState.None,
89+
{
90+
command: 'vscode-task.goToDefinition',
91+
title: 'Go to Definition',
92+
arguments: [task, true]
93+
}
94+
);
95+
taskTreeItems = taskTreeItems.concat(item);
96+
}
97+
98+
if (namespaceName in namespaceMap && namespaceMap[namespaceName] !== null) {
99+
let namespaceTreeItem = namespaceTreeItems.get(namespaceName);
75100

76-
// Check if the task has a namespace
77-
// If it does, add it to the namespace/tasks map
78-
if (this.nestingEnabled && namespacePath !== "") {
79-
let namespaceLabel = getNamespaceLabel(namespacePath);
80-
let namespaceTreeItem = namespaceTreeItems.get(namespaceLabel) ?? new elements.NamespaceTreeItem(
81-
namespaceLabel,
101+
if (namespaceTreeItem === undefined) {
102+
namespaceTreeItem = new elements.NamespaceTreeItem(
103+
namespaceName,
82104
workspace,
83-
fullNamespacePath,
105+
namespaceMap[namespaceName],
84106
[],
85107
vscode.TreeItemCollapsibleState.Collapsed
86108
);
87-
namespaceTreeItem.tasks.push(task);
88-
namespaceTreeItems.set(namespaceLabel, namespaceTreeItem);
89-
}
90-
91-
// Otherwise, create a tree item for the task
92-
else {
93-
let taskLabel = getTaskLabel(task, this.nestingEnabled);
94-
let taskTreeItem = new elements.TaskTreeItem(
95-
taskLabel,
96-
workspace,
97-
task,
98-
vscode.TreeItemCollapsibleState.None,
99-
{
100-
command: 'vscode-task.goToDefinition',
101-
title: 'Go to Definition',
102-
arguments: [task, true]
103-
}
104-
);
105-
taskTreeItems = taskTreeItems.concat(taskTreeItem);
106109
}
110+
namespaceTreeItem.tasks.push(task);
111+
namespaceTreeItems.set(namespaceName, namespaceTreeItem);
107112
}
108113

109-
// Add the namespace and tasks to the tree
110-
namespaceTreeItems.forEach(namespace => {
111-
treeItems = treeItems.concat(namespace);
112-
});
113-
treeItems = treeItems.concat(taskTreeItems);
114+
});
114115

115-
return Promise.resolve(treeItems);
116-
}
116+
// Add the namespace and tasks to the tree
117+
namespaceTreeItems.forEach(namespace => {
118+
treeItems = treeItems.concat(namespace);
119+
});
120+
treeItems = treeItems.concat(taskTreeItems);
117121

118122
return Promise.resolve(treeItems);
119123
}
@@ -136,6 +140,37 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.Tr
136140
refresh(taskfiles?: models.Taskfile[]): void {
137141
if (taskfiles) {
138142
this._taskfiles = taskfiles;
143+
this._treeViewMap = {};
144+
145+
// loop over all of the tasks in all of the task files and map their names into a set
146+
const taskNames = Array.from(new Set(
147+
taskfiles.flatMap(taskfile =>
148+
taskfile.tasks.flatMap(task => task.name)
149+
)
150+
// and sort desc so we know that the namespace reduction sets child objects correctly.
151+
)).sort((a, b) => (a > b ? -1 : 1));
152+
153+
taskNames.reduce((acc: any, key: string) => {
154+
const parts = key.split(':');
155+
let currentLevel = acc;
156+
157+
parts.forEach((part, index) => {
158+
if (part === "") {
159+
return;
160+
};
161+
162+
if (!(part in currentLevel)) {
163+
currentLevel[part] = {};
164+
if (index === parts.length - 1) {
165+
currentLevel[part] = null;
166+
}
167+
}
168+
169+
currentLevel = currentLevel[part] as models.TaskMapping;
170+
});
171+
172+
return acc;
173+
}, this._treeViewMap);
139174
}
140175
this._onDidChangeTreeData.fire(undefined);
141176
}
@@ -151,31 +186,24 @@ function getFullNamespacePath(task: models.Task): string {
151186
}
152187

153188
function trimParentNamespace(namespace: string, parentNamespace: string): string {
154-
if (namespace === parentNamespace) {
155-
return "";
189+
if (parentNamespace === "") {
190+
return namespace;
156191
}
157-
parentNamespace += namespaceSeparator;
158-
// If the namespace is a direct child of the parent namespace, remove the parent namespace
159-
if (namespace.startsWith(parentNamespace)) {
160-
return namespace.substring(parentNamespace.length);
192+
193+
const index = namespace.indexOf(parentNamespace + namespaceSeparator);
194+
195+
if (index === -1) {
196+
return namespace;
161197
}
162-
return namespace;
198+
199+
return namespace.substring(index + parentNamespace.length + 1);
163200
}
164201

165-
function getNamespaceLabel(namespacePath: string): string {
202+
function getNamespaceName(namespacePath: string): string {
166203
// If the namespace has no separator, return the namespace
167204
if (!namespacePath.includes(namespaceSeparator)) {
168205
return namespacePath;
169206
}
170207
// Return the first element of the namespace
171208
return namespacePath.substring(0, namespacePath.indexOf(namespaceSeparator));
172209
}
173-
174-
function getTaskLabel(task: models.Task, nestingEnabled: boolean): string {
175-
// If the task has no namespace, return the task's name
176-
if (!task.name.includes(namespaceSeparator) || !nestingEnabled) {
177-
return task.name;
178-
}
179-
// Return the task's name by removing the namespace
180-
return task.name.substring(task.name.lastIndexOf(namespaceSeparator) + 1);
181-
}

src/task.ts

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as elements from './elements';
3-
import * as services from './services';
43
import * as models from './models';
4+
import * as services from './services';
55
import { log, settings } from './utils';
66

77
export class TaskExtension {
@@ -18,40 +18,42 @@ export class TaskExtension {
1818

1919
public async update(checkForUpdates?: boolean): Promise<void> {
2020
// Do version checks
21-
await services.taskfile.checkInstallation(checkForUpdates).then((status): Promise<PromiseSettledResult<models.Taskfile | undefined>[]> => {
21+
await services.taskfile.checkInstallation(checkForUpdates).then(
22+
(status): Promise<PromiseSettledResult<models.Taskfile | undefined>[]> => {
2223

23-
// Set the status
24-
vscode.commands.executeCommand('setContext', 'vscode-task:status', status);
24+
// Set the status
25+
vscode.commands.executeCommand('setContext', 'vscode-task:status', status);
2526

26-
// If the status is not "ready", reject the promise
27-
if (status !== "ready") {
28-
return Promise.reject();
29-
}
27+
// If the status is not "ready", reject the promise
28+
if (status !== "ready") {
29+
return Promise.reject();
30+
}
3031

31-
// Read taskfiles
32-
let p: Promise<models.Taskfile | undefined>[] = [];
33-
vscode.workspace.workspaceFolders?.forEach((folder) => {
34-
p.push(services.taskfile.read(folder.uri.fsPath));
32+
// Read taskfiles
33+
let p: Promise<models.Taskfile | undefined>[] = [];
34+
vscode.workspace.workspaceFolders?.forEach((folder) => {
35+
p.push(services.taskfile.read(folder.uri.fsPath));
36+
});
37+
38+
return Promise.allSettled(p);
39+
40+
// If there are no valid taskfiles, set the status to "noTaskfile"
41+
}).then(results => {
42+
this._taskfiles = results
43+
.filter(result => result.status === "fulfilled")
44+
.map(result => <PromiseFulfilledResult<any>>result)
45+
.map(result => result.value)
46+
.filter(value => value !== undefined);
47+
let rejected = results
48+
.filter(result => result.status === "rejected")
49+
.map(result => <PromiseRejectedResult>result)
50+
.map(result => result.reason);
51+
if (rejected.length > 0) {
52+
vscode.commands.executeCommand('setContext', 'vscode-task:status', "error");
53+
} else if (this._taskfiles.length === 0) {
54+
vscode.commands.executeCommand('setContext', 'vscode-task:status', "noTaskfile");
55+
}
3556
});
36-
return Promise.allSettled(p);
37-
38-
// If there are no valid taskfiles, set the status to "noTaskfile"
39-
}).then(results => {
40-
this._taskfiles = results
41-
.filter(result => result.status === "fulfilled")
42-
.map(result => <PromiseFulfilledResult<any>>result)
43-
.map(result => result.value)
44-
.filter(value => value !== undefined);
45-
let rejected = results
46-
.filter(result => result.status === "rejected")
47-
.map(result => <PromiseRejectedResult>result)
48-
.map(result => result.reason);
49-
if (rejected.length > 0) {
50-
vscode.commands.executeCommand('setContext', 'vscode-task:status', "error");
51-
} else if (this._taskfiles.length === 0) {
52-
vscode.commands.executeCommand('setContext', 'vscode-task:status', "noTaskfile");
53-
}
54-
});
5557
}
5658

5759
public async refresh(checkForUpdates?: boolean): Promise<void> {
@@ -68,7 +70,6 @@ export class TaskExtension {
6870
}
6971

7072
public registerCommands(context: vscode.ExtensionContext): void {
71-
7273
// Initialise Taskfile
7374
context.subscriptions.push(vscode.commands.registerCommand('vscode-task.init', () => {
7475
log.info("Command: vscode-task.init");
@@ -119,19 +120,13 @@ export class TaskExtension {
119120
// Run task picker
120121
context.subscriptions.push(vscode.commands.registerCommand('vscode-task.runTaskPicker', () => {
121122
log.info("Command: vscode-task.runTaskPicker");
122-
let items: vscode.QuickPickItem[] = [];
123-
this._taskfiles.forEach(taskfile => {
124-
if (taskfile.tasks.length > 0) {
125-
items = items.concat(new elements.QuickPickTaskSeparator(taskfile));
126-
taskfile.tasks.forEach(task => {
127-
items = items.concat(new elements.QuickPickTaskItem(taskfile, task));
128-
});
129-
}
130-
});
123+
let items: vscode.QuickPickItem[] = this._loadTasksFromTaskfile();
124+
131125
if (items.length === 0) {
132126
vscode.window.showInformationMessage('No tasks found');
133127
return;
134128
}
129+
135130
vscode.window.showQuickPick(items).then((item) => {
136131
if (item && item instanceof elements.QuickPickTaskItem) {
137132
services.taskfile.runTask(item.label, item.taskfile.workspace);
@@ -208,6 +203,19 @@ export class TaskExtension {
208203
vscode.workspace.onDidChangeConfiguration(event => { this._onDidChangeConfiguration(event); });
209204
}
210205

206+
private _loadTasksFromTaskfile() {
207+
let items: vscode.QuickPickItem[] = [];
208+
this._taskfiles.forEach(taskfile => {
209+
if (taskfile.tasks.length > 0) {
210+
items = items.concat(new elements.QuickPickTaskSeparator(taskfile));
211+
taskfile.tasks.forEach(task => {
212+
items = items.concat(new elements.QuickPickTaskItem(taskfile, task));
213+
});
214+
}
215+
});
216+
return items;
217+
}
218+
211219
private async _onDidTaskfileChange() {
212220
log.info("Detected changes to taskfile");
213221

0 commit comments

Comments
 (0)