Skip to content

Commit

Permalink
enable workspace-scoped tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin Grant committed Jan 20, 2021
1 parent 4a3e133 commit 224091a
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 73 deletions.
112 changes: 112 additions & 0 deletions examples/api-tests/src/task-configurations.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// @ts-check

describe('The Task Configuration Manager', function () {
this.timeout(5000);

const { assert } = chai;

const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { TaskScope, TaskConfigurationScope } = require('@theia/task/lib/common/task-protocol');
const { TaskConfigurationManager } = require('@theia/task/lib/browser/task-configuration-manager');
const container = window.theia.container;
const workspaceService = container.get(WorkspaceService);
const taskConfigurationManager = container.get(TaskConfigurationManager);

const baseWorkspaceURI = workspaceService.tryGetRoots()[0].resource;
const baseWorkspaceRoot = baseWorkspaceURI.toString();

const basicTaskConfig = {
label: 'task',
type: 'shell',
command: 'top',
};

/** @type {Set<TaskConfigurationScope>} */
const scopesToClear = new Set();

describe('in a single-root workspace', () => {
beforeEach(() => clearTasks());
after(() => clearTasks());

setAndRetrieveTasks(() => TaskScope.Global, 'user');
setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder');
});

async function clearTasks() {
await Promise.all(Array.from(scopesToClear, async scope => {
if (!!scope || scope === 0) {
await taskConfigurationManager.setTaskConfigurations(scope, []);
}
}));
scopesToClear.clear();
}

/**
* @param {() => TaskConfigurationScope} scopeGenerator a function to allow lazy evaluation of the second workspace root.
* @param {string} scopeLabel
* @param {boolean} only
*/
function setAndRetrieveTasks(scopeGenerator, scopeLabel, only = false) {
const testFunction = only ? it.only : it;
testFunction(`successfully handles ${scopeLabel} scope`, async () => {
const scope = scopeGenerator();
scopesToClear.add(scope);
const initialTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(initialTasks, []);
await taskConfigurationManager.setTaskConfigurations(scope, [basicTaskConfig]);
const newTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(newTasks, [basicTaskConfig]);
});
}

/* UNCOMMENT TO RUN MULTI-ROOT TESTS */
// const { FileService } = require('@theia/filesystem/lib/browser/file-service');
// const { EnvVariablesServer } = require('@theia/core/lib/common/env-variables');
// const URI = require('@theia/core/lib/common/uri').default;

// const fileService = container.get(FileService);
// /** @type {EnvVariablesServer} */
// const envVariables = container.get(EnvVariablesServer);

// describe('in a multi-root workspace', () => {
// let secondWorkspaceRoot = '';
// before(async () => {
// const configLocation = await envVariables.getConfigDirUri();
// const secondWorkspaceRootURI = new URI(configLocation).parent.resolve(`test-root-${Date.now()}`);
// secondWorkspaceRoot = secondWorkspaceRootURI.toString();
// await fileService.createFolder(secondWorkspaceRootURI);
// /** @type {Promise<void>} */
// const waitForEvent = new Promise(resolve => {
// const listener = taskConfigurationManager.onDidChangeTaskConfig(() => {
// listener.dispose();
// resolve();
// });
// });
// workspaceService.addRoot(secondWorkspaceRootURI);
// return waitForEvent;
// });
// beforeEach(() => clearTasks());
// after(() => clearTasks());
// setAndRetrieveTasks(() => TaskScope.Global, 'user');
// setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
// setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder (1)');
// setAndRetrieveTasks(() => secondWorkspaceRoot, 'folder (2)');
// });
});
3 changes: 3 additions & 0 deletions packages/debug/src/browser/debug-schema-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import URI from '@theia/core/lib/common/uri';
import { DebugService } from '../common/debug-service';
import { debugPreferencesSchema } from './debug-preferences';
import { inputsSchema } from '@theia/variable-resolver/lib/browser/variable-input-schema';
import { WorkspaceService } from '@theia/workspace/lib/browser';

@injectable()
export class DebugSchemaUpdater implements JsonSchemaContribution {

protected readonly uri = new URI(launchSchemaId);

@inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(DebugService) protected readonly debug: DebugService;

@postConstruct()
Expand All @@ -41,6 +43,7 @@ export class DebugSchemaUpdater implements JsonSchemaContribution {
fileMatch: ['launch.json'],
url: this.uri.toString()
});
this.workspaceService.updateSchema('launch', { $ref: this.uri.toString() });
}

async update(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { JsonSchemaRegisterContext, JsonSchemaContribution } from '@theia/core/l
import { PreferenceSchemaProvider } from '@theia/core/lib/browser/preferences/preference-contribution';
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';
import { PreferenceScope } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';

const PREFERENCE_URI_PREFIX = 'vscode://schemas/settings/';
const USER_STORAGE_PREFIX = 'user_storage:/';
Expand All @@ -38,12 +39,16 @@ export class PreferencesJsonSchemaContribution implements JsonSchemaContribution
@inject(PreferenceConfigurations)
protected readonly preferenceConfigurations: PreferenceConfigurations;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

registerSchemas(context: JsonSchemaRegisterContext): void {
this.registerSchema(PreferenceScope.Default, context);
this.registerSchema(PreferenceScope.User, context);
this.registerSchema(PreferenceScope.Workspace, context);
this.registerSchema(PreferenceScope.Folder, context);

this.workspaceService.updateSchema('settings', { $ref: this.getSchemaURIForScope(PreferenceScope.Workspace).toString() });
this.schemaProvider.onDidPreferenceSchemaChanged(() => this.updateInMemoryResources());
}

Expand All @@ -60,16 +65,20 @@ export class PreferencesJsonSchemaContribution implements JsonSchemaContribution
}

private updateInMemoryResources(): void {
this.inmemoryResources.update(new URI(PREFERENCE_URI_PREFIX + PreferenceScope[PreferenceScope.Default].toLowerCase()),
this.inmemoryResources.update(this.getSchemaURIForScope(PreferenceScope.Default),
this.serializeSchema(+PreferenceScope.Default));
this.inmemoryResources.update(new URI(PREFERENCE_URI_PREFIX + PreferenceScope[PreferenceScope.User].toLowerCase()),
this.inmemoryResources.update(this.getSchemaURIForScope(PreferenceScope.User),
this.serializeSchema(+PreferenceScope.User));
this.inmemoryResources.update(new URI(PREFERENCE_URI_PREFIX + PreferenceScope[PreferenceScope.Workspace].toLowerCase()),
this.inmemoryResources.update(this.getSchemaURIForScope(PreferenceScope.Workspace),
this.serializeSchema(+PreferenceScope.Workspace));
this.inmemoryResources.update(new URI(PREFERENCE_URI_PREFIX + PreferenceScope[PreferenceScope.Folder].toLowerCase()),
this.inmemoryResources.update(this.getSchemaURIForScope(PreferenceScope.Folder),
this.serializeSchema(+PreferenceScope.Folder));
}

private getSchemaURIForScope(scope: PreferenceScope): URI {
return new URI(PREFERENCE_URI_PREFIX + PreferenceScope[scope].toLowerCase());
}

private getFileMatch(scope: string): string[] {
const baseName = this.preferenceConfigurations.getConfigName() + '.json';
return [baseName, new URI(USER_STORAGE_PREFIX + scope).resolve(baseName).toString()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export abstract class SectionPreferenceProvider extends AbstractResourcePreferen
if (preferenceName === this.section) {
return [];
}
if (preferenceName.startsWith(this.section + '.')) {
return [preferenceName.substr(this.section!.length + 1)];
if (preferenceName.startsWith(`${this.section}.`)) {
return [preferenceName.slice(this.section.length + 1)];
}
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,20 @@ export class WorkspaceFilePreferenceProvider extends AbstractResourcePreferenceP
protected parse(content: string): any {
const data = super.parse(content);
if (WorkspaceData.is(data)) {
return data.settings || {};
const settings = { ...data.settings };
for (const key of this.configurations.getSectionNames().filter(name => name !== 'settings')) {
settings[key] = data[key];
}
return settings;
}
return {};
}

protected getPath(preferenceName: string): string[] {
const firstSegment = preferenceName.split('.')[0];
if (firstSegment && this.configurations.isSectionName(firstSegment)) {
return [firstSegment, preferenceName.slice(firstSegment.length + 1)];
}
return ['settings', preferenceName];
}

Expand Down
76 changes: 44 additions & 32 deletions packages/task/src/browser/task-configuration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/work
import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { DisposableCollection } from '@theia/core/lib/common';

export interface TasksChange {
scope: TaskConfigurationScope;
Expand Down Expand Up @@ -59,6 +60,9 @@ export class TaskConfigurationManager {
@inject(PreferenceProvider) @named(PreferenceScope.User)
protected readonly userPreferences: PreferenceProvider;

@inject(PreferenceProvider) @named(PreferenceScope.Workspace)
protected readonly workspacePreferences: PreferenceProvider;

@inject(PreferenceConfigurations)
protected readonly preferenceConfigurations: PreferenceConfigurations;

Expand All @@ -71,14 +75,12 @@ export class TaskConfigurationManager {
protected readonly onDidChangeTaskConfigEmitter = new Emitter<TasksChange>();
readonly onDidChangeTaskConfig: Event<TasksChange> = this.onDidChangeTaskConfigEmitter.event;

protected readonly models = new Map<string, TaskConfigurationModel>();
protected userModel: TaskConfigurationModel;
protected readonly models = new Map<TaskConfigurationScope, TaskConfigurationModel>();
protected workspaceDelegate: PreferenceProvider;

@postConstruct()
protected async init(): Promise<void> {
this.userModel = new TaskConfigurationModel(TaskScope.Global, this.userPreferences);
this.userModel.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Global, type: FileChangeType.UPDATED }));
this.updateModels();
this.createModels();
this.folderPreferences.onDidPreferencesChanged(e => {
if (e['tasks']) {
this.updateModels();
Expand All @@ -89,9 +91,21 @@ export class TaskConfigurationManager {
});
}

protected createModels(): void {
const userModel = new TaskConfigurationModel(TaskScope.Global, this.userPreferences);
userModel.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Global, type: FileChangeType.UPDATED }));
this.models.set(TaskScope.Global, userModel);

this.updateModels();
}

protected updateModels = debounce(async () => {
const roots = await this.workspaceService.roots;
const toDelete = new Set(this.models.keys());
const toDelete = new Set(
[...this.models.keys()]
.filter(key => key !== TaskScope.Global && key !== TaskScope.Workspace)
);
this.updateWorkspaceModel();
for (const rootStat of roots) {
const key = rootStat.resource.toString();
toDelete.delete(key);
Expand All @@ -113,23 +127,11 @@ export class TaskConfigurationManager {
}, 500);

getTasks(scope: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] {
if (typeof scope === 'string' && this.models.has(scope)) {
const taskPrefModel = this.models.get(scope)!;
return taskPrefModel.configurations;
}
return this.userModel.configurations;
return this.getModel(scope)?.configurations ?? [];
}

getTask(name: string, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration | undefined {
const taskPrefModel = this.getModel(scope);
if (taskPrefModel) {
for (const configuration of taskPrefModel.configurations) {
if (configuration.name === name) {
return configuration;
}
}
}
return this.userModel.configurations.find(configuration => configuration.name === 'name');
return this.getTasks(scope).find((configuration: TaskCustomization | TaskConfiguration) => configuration.name === name);
}

async openConfiguration(scope: TaskConfigurationScope): Promise<void> {
Expand All @@ -156,18 +158,8 @@ export class TaskConfigurationManager {
return false;
}

private getModel(scope: TaskConfigurationScope): TaskConfigurationModel | undefined {
if (!scope) {
return undefined;
}
for (const model of this.models.values()) {
if (model.scope === scope) {
return model;
}
}
if (scope === TaskScope.Global) {
return this.userModel;
}
protected getModel(scope: TaskConfigurationScope): TaskConfigurationModel | undefined {
return this.models.get(scope);
}

protected async doOpen(model: TaskConfigurationModel): Promise<EditorWidget | undefined> {
Expand Down Expand Up @@ -204,6 +196,26 @@ export class TaskConfigurationManager {
return selected.content;
}
}

protected readonly toDisposeOnDelegateChange = new DisposableCollection();
protected updateWorkspaceModel(): void {
const newDelegate = this.workspaceService.saved ? this.workspacePreferences : this.folderPreferences;
const effectiveScope = this.workspaceService.saved ? TaskScope.Workspace : this.workspaceService.tryGetRoots()[0]?.resource.toString();
if (newDelegate !== this.workspaceDelegate) {
this.workspaceDelegate = newDelegate;
this.toDisposeOnDelegateChange.dispose();

const workspaceModel = new TaskConfigurationModel(effectiveScope, newDelegate);
this.toDisposeOnDelegateChange.push(workspaceModel);
// If the delegate is the folder preference provider, its events will be relayed via the folder scope models.
if (newDelegate === this.workspacePreferences) {
this.toDisposeOnDelegateChange.push(workspaceModel.onDidChange(() => {
this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Workspace, type: FileChangeType.UPDATED });
}));
}
this.models.set(TaskScope.Workspace, workspaceModel);
}
}
}

export namespace TaskConfigurationManager {
Expand Down
2 changes: 1 addition & 1 deletion packages/task/src/browser/task-configuration-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class TaskConfigurationModel implements Disposable {
);

constructor(
readonly scope: TaskConfigurationScope,
protected readonly scope: TaskConfigurationScope,
readonly preferences: PreferenceProvider
) {
this.reconcile();
Expand Down
Loading

0 comments on commit 224091a

Please sign in to comment.