Skip to content

Commit

Permalink
fix(go-task#89, go-task#59): Fix Namespace Tree Mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxCheetham committed Sep 2, 2023
1 parent c5ac2e9 commit fecdb69
Show file tree
Hide file tree
Showing 6 changed files with 1,185 additions and 513 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@
"@vscode/test-electron": "^2.2.3",
"@vscode/vsce": "^2.19.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint": "^8.2.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "^2.25.2",
"glob": "^8.1.0",
"mocha": "^10.2.0",
"ovsx": "^0.8.1",
Expand Down
2 changes: 1 addition & 1 deletion src/elements/treeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class NamespaceTreeItem extends vscode.TreeItem {
constructor(
readonly label: string,
readonly workspace: string,
readonly namespace: string,
readonly namespaceMap: any,
readonly tasks: models.Task[],
readonly collapsibleState: vscode.TreeItemCollapsibleState,
readonly command?: vscode.Command
Expand Down
4 changes: 4 additions & 0 deletions src/models/taskfile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export type TaskMapping = {
[key: string]: TaskMapping | null;
};

export interface Taskfile {
tasks: Task[];
location: string; // The location of the actual Taskfile
Expand Down
146 changes: 87 additions & 59 deletions src/providers/taskTreeDataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as models from '../models';
import * as elements from '../elements';
import * as path from 'path';
import * as models from '../models';

const namespaceSeparator = ':';

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

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

var tasks: models.Task[] | undefined;
var parentNamespace = "";
var namespaceMap = this._treeViewMap;
var workspace = "";

// If there is no parent and exactly one workspace folder or if the parent is a workspace
Expand All @@ -61,59 +63,61 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.Tr
// If there is a parent and it is a namespace
if (parent instanceof elements.NamespaceTreeItem) {
tasks = parent.tasks;
parentNamespace = parent.namespace;
parentNamespace = parent.label;
namespaceMap = parent.namespaceMap;
workspace = parent.workspace;
}

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

let fullNamespacePath = getFullNamespacePath(task);
let namespacePath = trimParentNamespace(fullNamespacePath, parentNamespace);
if (tasks === undefined) {
return Promise.resolve([]);
}

let namespaceTreeItems = new Map<string, elements.NamespaceTreeItem>();
let taskTreeItems: elements.TaskTreeItem[] = [];
tasks.forEach(task => {
let taskName = task.name.split(":").pop() ?? task.name;
let namespacePath = trimParentNamespace(task.name, parentNamespace);
let namespaceName = getNamespaceName(namespacePath);

if (taskName in namespaceMap) {
let item = new elements.TaskTreeItem(
task.name.split(namespaceSeparator).pop() ?? task.name,
workspace,
task,
vscode.TreeItemCollapsibleState.None,
{
command: 'vscode-task.goToDefinition',
title: 'Go to Definition',
arguments: [task, true]
}
);
taskTreeItems = taskTreeItems.concat(item);
}

if (namespaceName in namespaceMap && namespaceMap[namespaceName] !== null) {
let namespaceTreeItem = namespaceTreeItems.get(namespaceName);

// Check if the task has a namespace
// If it does, add it to the namespace/tasks map
if (this.nestingEnabled && namespacePath !== "") {
let namespaceLabel = getNamespaceLabel(namespacePath);
let namespaceTreeItem = namespaceTreeItems.get(namespaceLabel) ?? new elements.NamespaceTreeItem(
namespaceLabel,
if (namespaceTreeItem === undefined) {
namespaceTreeItem = new elements.NamespaceTreeItem(
namespaceName,
workspace,
fullNamespacePath,
namespaceMap[namespaceName],
[],
vscode.TreeItemCollapsibleState.Collapsed
);
namespaceTreeItem.tasks.push(task);
namespaceTreeItems.set(namespaceLabel, namespaceTreeItem);
}

// Otherwise, create a tree item for the task
else {
let taskLabel = getTaskLabel(task, this.nestingEnabled);
let taskTreeItem = new elements.TaskTreeItem(
taskLabel,
workspace,
task,
vscode.TreeItemCollapsibleState.None,
{
command: 'vscode-task.goToDefinition',
title: 'Go to Definition',
arguments: [task, true]
}
);
taskTreeItems = taskTreeItems.concat(taskTreeItem);
}
namespaceTreeItem.tasks.push(task);
namespaceTreeItems.set(namespaceName, namespaceTreeItem);
}

// Add the namespace and tasks to the tree
namespaceTreeItems.forEach(namespace => {
treeItems = treeItems.concat(namespace);
});
treeItems = treeItems.concat(taskTreeItems);
});

return Promise.resolve(treeItems);
}
// Add the namespace and tasks to the tree
namespaceTreeItems.forEach(namespace => {
treeItems = treeItems.concat(namespace);
});
treeItems = treeItems.concat(taskTreeItems);

return Promise.resolve(treeItems);
}
Expand All @@ -136,6 +140,37 @@ export class TaskTreeDataProvider implements vscode.TreeDataProvider<elements.Tr
refresh(taskfiles?: models.Taskfile[]): void {
if (taskfiles) {
this._taskfiles = taskfiles;
this._treeViewMap = {};

// loop over all of the tasks in all of the task files and map their names into a set
const taskNames = Array.from(new Set(
taskfiles.flatMap(taskfile =>
taskfile.tasks.flatMap(task => task.name)
)
// and sort desc so we know that the namespace reduction sets child objects correctly.
)).sort((a, b) => (a > b ? -1 : 1));

taskNames.reduce((acc: any, key: string) => {
const parts = key.split(':');
let currentLevel = acc;

parts.forEach((part, index) => {
if (part === "") {
return;
};

if (!(part in currentLevel)) {
currentLevel[part] = {};
if (index === parts.length - 1) {
currentLevel[part] = null;
}
}

currentLevel = currentLevel[part] as models.TaskMapping;
});

return acc;
}, this._treeViewMap);
}
this._onDidChangeTreeData.fire(undefined);
}
Expand All @@ -151,31 +186,24 @@ function getFullNamespacePath(task: models.Task): string {
}

function trimParentNamespace(namespace: string, parentNamespace: string): string {
if (namespace === parentNamespace) {
return "";
if (parentNamespace === "") {
return namespace;
}
parentNamespace += namespaceSeparator;
// If the namespace is a direct child of the parent namespace, remove the parent namespace
if (namespace.startsWith(parentNamespace)) {
return namespace.substring(parentNamespace.length);

const index = namespace.indexOf(parentNamespace + namespaceSeparator);

if (index === -1) {
return namespace;
}
return namespace;

return namespace.substring(index + parentNamespace.length + 1);
}

function getNamespaceLabel(namespacePath: string): string {
function getNamespaceName(namespacePath: string): string {
// If the namespace has no separator, return the namespace
if (!namespacePath.includes(namespaceSeparator)) {
return namespacePath;
}
// Return the first element of the namespace
return namespacePath.substring(0, namespacePath.indexOf(namespaceSeparator));
}

function getTaskLabel(task: models.Task, nestingEnabled: boolean): string {
// If the task has no namespace, return the task's name
if (!task.name.includes(namespaceSeparator) || !nestingEnabled) {
return task.name;
}
// Return the task's name by removing the namespace
return task.name.substring(task.name.lastIndexOf(namespaceSeparator) + 1);
}
90 changes: 49 additions & 41 deletions src/task.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as elements from './elements';
import * as services from './services';
import * as models from './models';
import * as services from './services';
import { log, settings } from './utils';

export class TaskExtension {
Expand All @@ -18,40 +18,42 @@ export class TaskExtension {

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

// Set the status
vscode.commands.executeCommand('setContext', 'vscode-task:status', status);
// Set the status
vscode.commands.executeCommand('setContext', 'vscode-task:status', status);

// If the status is not "ready", reject the promise
if (status !== "ready") {
return Promise.reject();
}
// If the status is not "ready", reject the promise
if (status !== "ready") {
return Promise.reject();
}

// Read taskfiles
let p: Promise<models.Taskfile | undefined>[] = [];
vscode.workspace.workspaceFolders?.forEach((folder) => {
p.push(services.taskfile.read(folder.uri.fsPath));
// Read taskfiles
let p: Promise<models.Taskfile | undefined>[] = [];
vscode.workspace.workspaceFolders?.forEach((folder) => {
p.push(services.taskfile.read(folder.uri.fsPath));
});

return Promise.allSettled(p);

// If there are no valid taskfiles, set the status to "noTaskfile"
}).then(results => {
this._taskfiles = results
.filter(result => result.status === "fulfilled")
.map(result => <PromiseFulfilledResult<any>>result)
.map(result => result.value)
.filter(value => value !== undefined);
let rejected = results
.filter(result => result.status === "rejected")
.map(result => <PromiseRejectedResult>result)
.map(result => result.reason);
if (rejected.length > 0) {
vscode.commands.executeCommand('setContext', 'vscode-task:status', "error");
} else if (this._taskfiles.length === 0) {
vscode.commands.executeCommand('setContext', 'vscode-task:status', "noTaskfile");
}
});
return Promise.allSettled(p);

// If there are no valid taskfiles, set the status to "noTaskfile"
}).then(results => {
this._taskfiles = results
.filter(result => result.status === "fulfilled")
.map(result => <PromiseFulfilledResult<any>>result)
.map(result => result.value)
.filter(value => value !== undefined);
let rejected = results
.filter(result => result.status === "rejected")
.map(result => <PromiseRejectedResult>result)
.map(result => result.reason);
if (rejected.length > 0) {
vscode.commands.executeCommand('setContext', 'vscode-task:status', "error");
} else if (this._taskfiles.length === 0) {
vscode.commands.executeCommand('setContext', 'vscode-task:status', "noTaskfile");
}
});
}

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

public registerCommands(context: vscode.ExtensionContext): void {

// Initialise Taskfile
context.subscriptions.push(vscode.commands.registerCommand('vscode-task.init', () => {
log.info("Command: vscode-task.init");
Expand Down Expand Up @@ -119,19 +120,13 @@ export class TaskExtension {
// Run task picker
context.subscriptions.push(vscode.commands.registerCommand('vscode-task.runTaskPicker', () => {
log.info("Command: vscode-task.runTaskPicker");
let items: vscode.QuickPickItem[] = [];
this._taskfiles.forEach(taskfile => {
if (taskfile.tasks.length > 0) {
items = items.concat(new elements.QuickPickTaskSeparator(taskfile));
taskfile.tasks.forEach(task => {
items = items.concat(new elements.QuickPickTaskItem(taskfile, task));
});
}
});
let items: vscode.QuickPickItem[] = this._loadTasksFromTaskfile();

if (items.length === 0) {
vscode.window.showInformationMessage('No tasks found');
return;
}

vscode.window.showQuickPick(items).then((item) => {
if (item && item instanceof elements.QuickPickTaskItem) {
services.taskfile.runTask(item.label, item.taskfile.workspace);
Expand Down Expand Up @@ -208,6 +203,19 @@ export class TaskExtension {
vscode.workspace.onDidChangeConfiguration(event => { this._onDidChangeConfiguration(event); });
}

private _loadTasksFromTaskfile() {
let items: vscode.QuickPickItem[] = [];
this._taskfiles.forEach(taskfile => {
if (taskfile.tasks.length > 0) {
items = items.concat(new elements.QuickPickTaskSeparator(taskfile));
taskfile.tasks.forEach(task => {
items = items.concat(new elements.QuickPickTaskItem(taskfile, task));
});
}
});
return items;
}

private async _onDidTaskfileChange() {
log.info("Detected changes to taskfile");

Expand Down
Loading

0 comments on commit fecdb69

Please sign in to comment.