Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Namespace Tree Mapping) #90

Merged
merged 1 commit into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@
"@vscode/test-electron": "^2.2.3",
"@vscode/vsce": "^2.19.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint": "^8.2.0",
"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