Skip to content

Commit

Permalink
access tasks configs as preferences
Browse files Browse the repository at this point in the history
- In current Theia, the task extension is responsible for reading,
writing, and watching `tasks.json` files. With this change, the task
extension does not access `tasks.json` files directly, and leaves the
work to the preference extension.
- resolves #5013

Signed-off-by: Liang Huang <[email protected]>
  • Loading branch information
Liang Huang committed Sep 26, 2019
1 parent fbd63c5 commit ec7a447
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 213 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Breaking changes:
- Extenders should implement `Disposable` for plugin main services to handle reconnection properly.
- Many APIs are refactored to return `Disposable`.
- [monaco] Added support for `monaco.languages.ResourceFileEdit`. [#4723](https://github.com/eclipse-theia/theia/issues/4723)
- [task] removed `watchedConfigFileUris`, `watchersMap` `watcherServer`, `fileSystem`, `configFileUris`, `watchConfigurationFile()` and `unwatchConfigurationFile()` from `TaskConfigurations` class. [6268](https://github.com/theia-ide/theia/pull/6268)
- [task] removed `configurationFileFound` from `TaskService` class. [6268](https://github.com/theia-ide/theia/pull/6268)

Misc:

Expand Down
2 changes: 2 additions & 0 deletions packages/task/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
"@theia/filesystem": "^0.10.0",
"@theia/markers": "^0.10.0",
"@theia/monaco": "^0.10.0",
"@theia/preferences": "^0.10.0",
"@theia/process": "^0.10.0",
"@theia/terminal": "^0.10.0",
"@theia/variable-resolver": "^0.10.0",
"@theia/workspace": "^0.10.0",
"jsonc-parser": "^2.0.2",
"p-debounce": "^2.1.0",
"vscode-uri": "^1.0.8"
},
"publishConfig": {
Expand Down
3 changes: 2 additions & 1 deletion packages/task/src/browser/task-action-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import { injectable, inject } from 'inversify';
import { TaskService } from './task-service';
import { TaskRunQuickOpenItem } from './quick-open-task';
import { QuickOpenBaseAction, QuickOpenItem, QuickOpenActionProvider, QuickOpenAction } from '@theia/core/lib/browser/quick-open';
import { QuickOpenBaseAction, QuickOpenItem } from '@theia/core/lib/browser/quick-open';
import { QuickOpenAction, QuickOpenActionProvider } from '@theia/core/lib/common/quick-open-model';
import { ThemeService } from '@theia/core/lib/browser/theming';

@injectable()
Expand Down
200 changes: 200 additions & 0 deletions packages/task/src/browser/task-configuration-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/********************************************************************************
* Copyright (C) 2019 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
********************************************************************************/

import debounce = require('p-debounce');
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
import { PreferenceService } from '@theia/core/lib/browser';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { TaskConfigurationModel } from './task-configuration-model';
import { TaskCustomization, TaskConfiguration } from '../common/task-protocol';
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common';
import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';

@injectable()
export class TaskConfigurationManager {

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(EditorManager)
protected readonly editorManager: EditorManager;

@inject(QuickPickService)
protected readonly quickPick: QuickPickService;

@inject(FileSystem)
protected readonly filesystem: FileSystem;

@inject(PreferenceService)
protected readonly preferences: PreferenceService;

@inject(PreferenceConfigurations)
protected readonly preferenceConfigurations: PreferenceConfigurations;

@inject(WorkspaceVariableContribution)
protected readonly workspaceVariables: WorkspaceVariableContribution;

protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;

protected readonly onChangedTaskConfigEmitter = new Emitter<FileChange>();
readonly onChangedTaskConfig: Event<FileChange> = this.onChangedTaskConfigEmitter.event;

@postConstruct()
protected async init(): Promise<void> {
this.updateModels();
this.preferences.onPreferenceChanged(e => {
if (e.preferenceName === 'tasks') {
this.updateModels();
}
});
}

protected readonly models = new Map<string, TaskConfigurationModel>();
protected updateModels = debounce(async () => {
const roots = await this.workspaceService.roots;
const toDelete = new Set(this.models.keys());
for (const rootStat of roots) {
const key = rootStat.uri;
toDelete.delete(key);
if (!this.models.has(key)) {
const model = new TaskConfigurationModel(key, this.preferences);
model.onDidChange(() => this.onChangedTaskConfigEmitter.fire({ uri: key, type: FileChangeType.ADDED }));
model.onDispose(() => this.models.delete(key));
this.models.set(key, model);
}
}
for (const uri of toDelete) {
const model = this.models.get(uri);
if (model) {
model.dispose();
}
this.onChangedTaskConfigEmitter.fire({ uri, type: FileChangeType.DELETED });
}
}, 500);

getTasks(sourceFolderUri: string): (TaskCustomization | TaskConfiguration)[] {
if (this.models.has(sourceFolderUri)) {
const taskPrefModel = this.models.get(sourceFolderUri)!;
return taskPrefModel.configurations;
}
return [];
}

getTask(name: string, sourceFolderUri: string | undefined): TaskCustomization | TaskConfiguration | undefined {
const taskPrefModel = this.getModel(sourceFolderUri);
if (taskPrefModel) {
for (const configuration of taskPrefModel.configurations) {
if (configuration.name === name) {
return configuration;
}
}
}
}

async openConfiguration(sourceFolderUri: string): Promise<void> {
const taskPrefModel = this.getModel(sourceFolderUri);
if (taskPrefModel) {
await this.doOpen(taskPrefModel);
}
}

async addTaskConfiguration(sourceFolderUri: string, taskConfig: TaskCustomization): Promise<void> {
const taskPrefModel = this.getModel(sourceFolderUri);
if (taskPrefModel) {
const configurations = taskPrefModel.configurations;
return this.setTaskConfigurations(sourceFolderUri, [...configurations, taskConfig]);
}
}

async setTaskConfigurations(sourceFolderUri: string, taskConfigs: (TaskCustomization | TaskConfiguration)[]): Promise<void> {
const taskPrefModel = this.getModel(sourceFolderUri);
if (taskPrefModel) {
return taskPrefModel.setConfigurations(taskConfigs);
}
}

private getModel(sourceFolderUri: string | undefined): TaskConfigurationModel | undefined {
if (!sourceFolderUri) {
return undefined;
}
for (const model of this.models.values()) {
if (model.workspaceFolderUri === sourceFolderUri) {
return model;
}
}
}

protected async doOpen(model: TaskConfigurationModel): Promise<EditorWidget> {
let uri = model.uri;
if (!uri) {
uri = await this.doCreate(model);
}
return this.editorManager.open(uri, {
mode: 'activate'
});
}

protected async doCreate(model: TaskConfigurationModel): Promise<URI> {
await this.preferences.set('tasks', {}); // create dummy tasks.json in the correct place
const { configUri } = this.preferences.resolve('tasks'); // get uri to write content to it
let uri: URI;
if (configUri && configUri.path.base === 'tasks.json') {
uri = configUri;
} else { // fallback
uri = new URI(model.workspaceFolderUri).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`);
}
const content = this.getInitialConfigurationContent();
const fileStat = await this.filesystem.getFileStat(uri.toString());
if (!fileStat) {
throw new Error(`file not found: ${uri.toString()}`);
}
try {
await this.filesystem.setContent(fileStat, content);
} catch (e) {
if (!FileSystemError.FileExists.is(e)) {
throw e;
}
}
return uri;
}

protected getInitialConfigurationContent(): string {
return `{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"version": "2.0.0",
"tasks": ${JSON.stringify([], undefined, ' ').split('\n').map(line => ' ' + line).join('\n').trim()}
}
`;
}

}

export namespace TaskConfigurationManager {
export interface Data {
current?: {
name: string
workspaceFolderUri?: string
}
}
}
93 changes: 93 additions & 0 deletions packages/task/src/browser/task-configuration-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/********************************************************************************
* Copyright (C) 2019 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
********************************************************************************/

import URI from '@theia/core/lib/common/uri';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { TaskCustomization, TaskConfiguration } from '../common/task-protocol';
import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser/preferences/preference-service';

export class TaskConfigurationModel implements Disposable {

protected json: TaskConfigurationModel.JsonContent;

protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;

protected readonly toDispose = new DisposableCollection(
this.onDidChangeEmitter
);

constructor(
public readonly workspaceFolderUri: string,
protected readonly preferences: PreferenceService
) {
this.reconcile();
this.toDispose.push(this.preferences.onPreferenceChanged(e => {
if (e.preferenceName === 'tasks' && e.affects(workspaceFolderUri)) {
this.reconcile();
}
}));
}

get uri(): URI | undefined {
return this.json.uri;
}

dispose(): void {
this.toDispose.dispose();
}
get onDispose(): Event<void> {
return this.toDispose.onDispose;
}

get configurations(): (TaskCustomization | TaskConfiguration)[] {
return this.json.configurations;
}

reconcile(): void {
this.json = this.parseConfigurations();
this.onDidChangeEmitter.fire(undefined);
}

setConfigurations(value: object): Promise<void> {
return this.preferences.set('tasks.tasks', value, PreferenceScope.Folder, this.workspaceFolderUri);
}

protected parseConfigurations(): TaskConfigurationModel.JsonContent {
const configurations: (TaskCustomization | TaskConfiguration)[] = [];
// tslint:disable-next-line:no-any
const { configUri, value } = this.preferences.resolve<any>('tasks', undefined, this.workspaceFolderUri);
if (value && typeof value === 'object' && 'tasks' in value) {
if (Array.isArray(value.tasks)) {
for (const taskConfig of value.tasks) {
configurations.push(taskConfig);
}
}
}
return {
uri: configUri,
configurations
};
}

}
export namespace TaskConfigurationModel {
export interface JsonContent {
uri?: URI;
configurations: (TaskCustomization | TaskConfiguration)[];
}
}
Loading

0 comments on commit ec7a447

Please sign in to comment.