From 8f66f5a724eb4075452cf75107e97ec4a51e6230 Mon Sep 17 00:00:00 2001 From: Liang Huang Date: Thu, 26 Sep 2019 07:04:22 -0400 Subject: [PATCH] access tasks configs as preferences - 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 --- CHANGELOG.md | 7 + packages/task/package.json | 2 + .../task/src/browser/task-action-provider.ts | 3 +- .../src/browser/task-configuration-manager.ts | 200 ++++++++++++++ .../src/browser/task-configuration-model.ts | 93 +++++++ .../task/src/browser/task-configurations.ts | 259 +++++------------- .../task-folder-preference-provider.ts | 42 +++ .../task/src/browser/task-frontend-module.ts | 4 + packages/task/src/browser/task-preferences.ts | 42 +++ .../task/src/browser/task-schema-updater.ts | 11 +- packages/task/src/browser/task-service.ts | 17 -- 11 files changed, 467 insertions(+), 213 deletions(-) create mode 100644 packages/task/src/browser/task-configuration-manager.ts create mode 100644 packages/task/src/browser/task-configuration-model.ts create mode 100644 packages/task/src/browser/task-folder-preference-provider.ts create mode 100644 packages/task/src/browser/task-preferences.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc1d9deef91f..9f5f607429e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## v0.12.0 + +Breaking changes: + +- [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) + ## v0.11.0 - [core] added ENTER event handler to the open button in explorer [#6158](https://github.com/eclipse-theia/theia/pull/6158) diff --git a/packages/task/package.json b/packages/task/package.json index 8d48e97202bdf..d0215c1c48e9c 100644 --- a/packages/task/package.json +++ b/packages/task/package.json @@ -8,11 +8,13 @@ "@theia/filesystem": "^0.11.0", "@theia/markers": "^0.11.0", "@theia/monaco": "^0.11.0", + "@theia/preferences": "^0.11.0", "@theia/process": "^0.11.0", "@theia/terminal": "^0.11.0", "@theia/variable-resolver": "^0.11.0", "@theia/workspace": "^0.11.0", "jsonc-parser": "^2.0.2", + "p-debounce": "^2.1.0", "vscode-uri": "^1.0.8" }, "publishConfig": { diff --git a/packages/task/src/browser/task-action-provider.ts b/packages/task/src/browser/task-action-provider.ts index 736330452f3fe..961a10ee44307 100644 --- a/packages/task/src/browser/task-action-provider.ts +++ b/packages/task/src/browser/task-action-provider.ts @@ -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() diff --git a/packages/task/src/browser/task-configuration-manager.ts b/packages/task/src/browser/task-configuration-manager.ts new file mode 100644 index 0000000000000..d59e0a30a2a59 --- /dev/null +++ b/packages/task/src/browser/task-configuration-manager.ts @@ -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(); + readonly onDidChange: Event = this.onDidChangeEmitter.event; + + protected readonly onChangedTaskConfigEmitter = new Emitter(); + readonly onChangedTaskConfig: Event = this.onChangedTaskConfigEmitter.event; + + @postConstruct() + protected async init(): Promise { + this.updateModels(); + this.preferences.onPreferenceChanged(e => { + if (e.preferenceName === 'tasks') { + this.updateModels(); + } + }); + } + + protected readonly models = new Map(); + 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 { + const taskPrefModel = this.getModel(sourceFolderUri); + if (taskPrefModel) { + await this.doOpen(taskPrefModel); + } + } + + async addTaskConfiguration(sourceFolderUri: string, taskConfig: TaskCustomization): Promise { + 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 { + 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 { + let uri = model.uri; + if (!uri) { + uri = await this.doCreate(model); + } + return this.editorManager.open(uri, { + mode: 'activate' + }); + } + + protected async doCreate(model: TaskConfigurationModel): Promise { + 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 + } + } +} diff --git a/packages/task/src/browser/task-configuration-model.ts b/packages/task/src/browser/task-configuration-model.ts new file mode 100644 index 0000000000000..c15763aace69f --- /dev/null +++ b/packages/task/src/browser/task-configuration-model.ts @@ -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(); + readonly onDidChange: Event = 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 { + 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 { + 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('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)[]; + } +} diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 5331ac9f133b0..c0faec6c3a293 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -18,16 +18,12 @@ import { inject, injectable, postConstruct } from 'inversify'; import { ContributedTaskConfiguration, TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; +import { TaskConfigurationManager } from './task-configuration-manager'; import { Disposable, DisposableCollection, ResourceProvider } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; -import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; -import { FileSystem } from '@theia/filesystem/lib/common'; -import * as jsoncparser from 'jsonc-parser'; -import { ParseError } from 'jsonc-parser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { open, OpenerService } from '@theia/core/lib/browser'; -import { Resource } from '@theia/core'; +import { /*open,*/ OpenerService } from '@theia/core/lib/browser'; export interface TaskConfigurationClient { /** @@ -54,9 +50,6 @@ export class TaskConfigurations implements Disposable { */ protected taskCustomizationMap = new Map(); - protected watchedConfigFileUris: string[] = []; - protected watchersMap = new Map(); // map of watchers for task config files, where the key is folder uri - /** last directory element under which we look for task config */ protected readonly TASKFILEPATH = '.theia'; /** task configuration file name */ @@ -85,34 +78,13 @@ export class TaskConfigurations implements Disposable { @inject(ProvidedTaskConfigurations) protected readonly providedTaskConfigurations: ProvidedTaskConfigurations; - constructor( - @inject(FileSystemWatcher) protected readonly watcherServer: FileSystemWatcher, - @inject(FileSystem) protected readonly fileSystem: FileSystem - ) { - this.toDispose.push(watcherServer); - this.toDispose.push( - this.watcherServer.onFilesChanged(async changes => { - try { - const watchedConfigFileChanges = changes.filter(change => - this.watchedConfigFileUris.some(fileUri => FileChangeEvent.isAffected([change], new URI(fileUri))) - ).map(relatedChange => ( - { uri: relatedChange.uri.toString(), type: relatedChange.type } - )); - if (watchedConfigFileChanges.length >= 0) { - await this.onDidTaskFileChange(watchedConfigFileChanges); - if (this.client) { - this.client.taskConfigurationChanged(this.getTaskLabels()); - } - } - } catch (err) { - console.error(err); - } - }) - ); + @inject(TaskConfigurationManager) + protected readonly taskConfigurationManager: TaskConfigurationManager; + + constructor() { this.toDispose.push(Disposable.create(() => { this.tasksMap.clear(); this.taskCustomizationMap.clear(); - this.watchersMap.clear(); this.rawTaskConfigurations.clear(); this.client = undefined; })); @@ -120,6 +92,18 @@ export class TaskConfigurations implements Disposable { @postConstruct() protected init(): void { + this.toDispose.push( + this.taskConfigurationManager.onChangedTaskConfig(async change => { + try { + await this.onDidTaskFileChange([change]); + if (this.client) { + this.client.taskConfigurationChanged(this.getTaskLabels()); + } + } catch (err) { + console.error(err); + } + }) + ); this.reorgnizeTasks(); this.toDispose.pushAll([ this.taskDefinitionRegistry.onDidRegisterTaskDefinition(() => this.reorgnizeTasks()), @@ -135,52 +119,6 @@ export class TaskConfigurations implements Disposable { this.toDispose.dispose(); } - get configFileUris(): string[] { - return this.watchedConfigFileUris; - } - - /** - * Triggers the watching of a potential task configuration file, under the given root URI. - * Returns whether a configuration file was found. - */ - async watchConfigurationFile(rootUri: string): Promise { - const configFileUri = this.getConfigFileUri(rootUri); - if (!this.watchedConfigFileUris.some(uri => uri === configFileUri)) { - this.watchedConfigFileUris.push(configFileUri); - const disposableWatcher = await this.watcherServer.watchFileChanges(new URI(configFileUri)); - const disposable = Disposable.create(() => { - disposableWatcher.dispose(); - this.watchersMap.delete(configFileUri); - const ind = this.watchedConfigFileUris.findIndex(uri => uri === configFileUri); - if (ind >= 0) { - this.watchedConfigFileUris.splice(ind, 1); - } - }); - this.watchersMap.set(configFileUri, disposable); - this.toDispose.push(disposable); - this.refreshTasks(configFileUri); - } - - if (await this.fileSystem.exists(configFileUri)) { - return true; - } else { - console.info(`Config file ${this.TASKFILE} does not exist under ${rootUri}`); - return false; - } - } - - /** - * Stops watchers added to a potential task configuration file. - * Returns whether a configuration file was being watched before this function gets called. - */ - unwatchConfigurationFile(configFileUri: string): boolean { - if (!this.watchersMap.has(configFileUri)) { - return false; - } - this.watchersMap.get(configFileUri)!.dispose(); - return true; - } - /** returns the list of known task labels */ getTaskLabels(): string[] { return Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.keys())), [] as string[]); @@ -239,7 +177,7 @@ export class TaskConfigurations implements Disposable { return []; } - const customizationInRootFolder = this.taskCustomizationMap.get(new URI(rootFolder).path.toString()); + const customizationInRootFolder = this.taskCustomizationMap.get(new URI(rootFolder).toString()); if (customizationInRootFolder) { return customizationInRootFolder.filter(c => c.type === type); } @@ -278,7 +216,6 @@ export class TaskConfigurations implements Disposable { /** * Called when a change, to a config file we watch, is detected. - * Triggers a reparse, if appropriate. */ protected async onDidTaskFileChange(fileChanges: FileChange[]): Promise { for (const change of fileChanges) { @@ -292,64 +229,39 @@ export class TaskConfigurations implements Disposable { } /** - * Tries to read the tasks from a config file and if it successes then updates the list of available tasks. - * If reading a config file wasn't successful then does nothing. + * Read the task configs from the task configuration manager, and updates the list of available tasks. */ - protected async refreshTasks(configFileUri: string): Promise { - const tasksArray = await this.readTasks(configFileUri); - if (tasksArray) { - // only clear tasks map when successful at parsing the config file - // this way we avoid clearing and re-filling it multiple times if the - // user is editing the file in the auto-save mode, having momentarily - // non-parsing JSON. - this.removeTasks(configFileUri); - this.removeTaskCustomizations(configFileUri); - - this.reorgnizeTasks(); - } - } + protected async refreshTasks(rootFolderUri: string): Promise { + await this.readTasks(rootFolderUri); - /** parses a config file and extracts the tasks launch configurations */ - protected async readTasks(uri: string): Promise { - if (!await this.fileSystem.exists(uri)) { - return undefined; - } else { - try { - const response = await this.fileSystem.resolveContent(uri); + this.removeTasks(rootFolderUri); + this.removeTaskCustomizations(rootFolderUri); - const strippedContent = jsoncparser.stripComments(response.content); - const errors: ParseError[] = []; - const rawTasks = jsoncparser.parse(strippedContent, errors); + this.reorgnizeTasks(); + } - if (errors.length) { - for (const e of errors) { - console.error(`Error parsing ${uri}: error: ${e.error}, length: ${e.length}, offset: ${e.offset}`); - } - } - const rootFolderUri = this.getSourceFolderFromConfigUri(uri); - if (this.rawTaskConfigurations.has(rootFolderUri)) { - this.rawTaskConfigurations.delete(rootFolderUri); - } - if (rawTasks && rawTasks['tasks']) { - const tasks = rawTasks['tasks'].map((t: TaskCustomization | TaskConfiguration) => { - if (this.isDetectedTask(t)) { - const def = this.getTaskDefinition(t); - return Object.assign(t, { - _source: def!.source, - _scope: this.getSourceFolderFromConfigUri(uri) - }); - } - return Object.assign(t, { _source: this.getSourceFolderFromConfigUri(uri) }); - }); - this.rawTaskConfigurations.set(rootFolderUri, tasks); - return tasks; - } else { - return []; - } - } catch (err) { - console.error(`Error(s) reading config file: ${uri}`); - } + /** parses a config file and extracts the tasks launch configurations */ + protected async readTasks(rootFolderUri: string): Promise<(TaskCustomization | TaskConfiguration)[] | undefined> { + const configArray = this.taskConfigurationManager.getTasks(rootFolderUri); + if (this.rawTaskConfigurations.has(rootFolderUri)) { + this.rawTaskConfigurations.delete(rootFolderUri); } + const tasks = configArray.map(config => { + if (this.isDetectedTask(config)) { + const def = this.getTaskDefinition(config); + return { + ...config, + _source: def!.source, + _scope: rootFolderUri + }; + } + return { + ...config, + _source: rootFolderUri + }; + }); + this.rawTaskConfigurations.set(rootFolderUri, tasks); + return tasks; } /** Adds given task to a config file and opens the file to provide ability to edit task configuration. */ @@ -365,14 +277,13 @@ export class TaskConfigurations implements Disposable { return; } - const configFileUri = this.getConfigFileUri(sourceFolderUri); const configuredAndCustomizedTasks = await this.getTasks(); if (!configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { - await this.saveTask(configFileUri, task); + await this.saveTask(sourceFolderUri, task); } try { - await open(this.openerService, new URI(configFileUri)); + this.taskConfigurationManager.openConfiguration(sourceFolderUri); } catch (e) { console.error(`Error occurred while opening: ${this.TASKFILE}.`, e); } @@ -413,28 +324,10 @@ export class TaskConfigurations implements Disposable { } /** Writes the task to a config file. Creates a config file if this one does not exist */ - async saveTask(configFileUri: string, task: TaskConfiguration): Promise { - if (configFileUri && !await this.fileSystem.exists(configFileUri)) { - await this.fileSystem.createFile(configFileUri); - } - + saveTask(sourceFolderUri: string, task: TaskConfiguration): Promise { const { _source, $ident, ...preparedTask } = task; const customizedTaskTemplate = this.getTaskCustomizationTemplate(task) || preparedTask; - try { - const response = await this.fileSystem.resolveContent(configFileUri); - const content = response.content; - - const formattingOptions = { tabSize: 4, insertSpaces: true, eol: '' }; - const edits = jsoncparser.modify(content, ['tasks', -1], customizedTaskTemplate, { formattingOptions }); - const result = jsoncparser.applyEdits(content, edits); - - const resource = await this.resourceProvider(new URI(configFileUri)); - Resource.save(resource, { content: result }); - } catch (e) { - const message = `Failed to save task configuration for ${task.label} task.`; - console.error(`${message} ${e.toString()}`); - return; - } + return this.taskConfigurationManager.addTaskConfiguration(sourceFolderUri, customizedTaskTemplate); } /** @@ -486,45 +379,29 @@ export class TaskConfigurations implements Disposable { console.error('Global task cannot be customized'); return; } - const configFileUri = this.getConfigFileUri(sourceFolderUri); const configuredAndCustomizedTasks = await this.getTasks(); if (configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { // task is already in `tasks.json` - try { - const content = (await this.fileSystem.resolveContent(configFileUri)).content; - const errors: ParseError[] = []; - const jsonTasks = jsoncparser.parse(content, errors).tasks; - if (errors.length > 0) { - for (const e of errors) { - console.error(`Error parsing ${configFileUri}: error: ${e.error}, length: ${e.length}, offset: ${e.offset}`); + const jsonTasks = this.taskConfigurationManager.getTasks(sourceFolderUri); + if (jsonTasks) { + const ind = jsonTasks.findIndex((t: TaskCustomization | TaskConfiguration) => { + if (t.type !== (task.taskType || task.type)) { + return false; } - } - if (jsonTasks) { - const ind = jsonTasks.findIndex((t: TaskConfiguration) => { - if (t.type !== (task.taskType || task.type)) { - return false; - } - const def = this.taskDefinitionRegistry.getDefinition(t); - if (def) { - return def.properties.all.every(p => t[p] === task[p]); - } - return t.label === task.label; - }); - const newTask = Object.assign(jsonTasks[ind], { problemMatcher: problemMatchers.map(name => name.startsWith('$') ? name : `$${name}`) }); - jsonTasks[ind] = newTask; - } - const updatedTasks = JSON.stringify({ tasks: jsonTasks }); - const formattingOptions = { tabSize: 4, insertSpaces: true, eol: '' }; - const edits = jsoncparser.format(updatedTasks, undefined, formattingOptions); - const updatedContent = jsoncparser.applyEdits(updatedTasks, edits); - const resource = await this.resourceProvider(new URI(configFileUri)); - Resource.save(resource, { content: updatedContent }); - } catch (e) { - console.error(`Failed to save task configuration for ${task.label} task. ${e.toString()}`); - return; + const def = this.taskDefinitionRegistry.getDefinition(t); + if (def) { + return def.properties.all.every(p => t[p] === task[p]); + } + return t.label === task.label; + }); + jsonTasks[ind] = { + ...jsonTasks[ind], + problemMatcher: problemMatchers.map(name => name.startsWith('$') ? name : `$${name}`) + }; } + this.taskConfigurationManager.setTaskConfigurations(sourceFolderUri, jsonTasks); } else { // task is not in `tasks.json` task.problemMatcher = problemMatchers; - this.saveTask(configFileUri, task); + this.saveTask(sourceFolderUri, task); } } diff --git a/packages/task/src/browser/task-folder-preference-provider.ts b/packages/task/src/browser/task-folder-preference-provider.ts new file mode 100644 index 0000000000000..5778a20d6c190 --- /dev/null +++ b/packages/task/src/browser/task-folder-preference-provider.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * 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 { injectable } from 'inversify'; +import { FolderPreferenceProvider } from '@theia/preferences/lib/browser/folder-preference-provider'; + +@injectable() +export class TaskFolderPreferenceProvider extends FolderPreferenceProvider { + + // tslint:disable-next-line:no-any + protected parse(content: string): any { + const tasks = super.parse(content); + if (tasks === undefined) { + return undefined; + } + return { tasks: { ...tasks } }; + } + + protected getPath(preferenceName: string): string[] | undefined { + if (preferenceName === 'tasks') { + return []; + } + if (preferenceName.startsWith('tasks.')) { + return [preferenceName.substr('tasks.'.length)]; + } + return undefined; + } + +} diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index 78ec9914b5c6f..ddd1af7a22789 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -33,6 +33,8 @@ import { TaskActionProvider, ConfigureTaskAction } from './task-action-provider' import { TaskDefinitionRegistry } from './task-definition-registry'; import { ProblemMatcherRegistry } from './task-problem-matcher-registry'; import { ProblemPatternRegistry } from './task-problem-pattern-registry'; +import { TaskConfigurationManager } from './task-configuration-manager'; +import { bindTaskPreferences } from './task-preferences'; import '../../src/browser/style/index.css'; import './tasks-monaco-contribution'; @@ -51,6 +53,7 @@ export default new ContainerModule(bind => { bind(TaskTerminateQuickOpen).toSelf().inSingletonScope(); bind(TaskConfigurations).toSelf().inSingletonScope(); bind(ProvidedTaskConfigurations).toSelf().inSingletonScope(); + bind(TaskConfigurationManager).toSelf().inSingletonScope(); bind(TaskServer).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); @@ -70,4 +73,5 @@ export default new ContainerModule(bind => { bind(TaskSchemaUpdater).toSelf().inSingletonScope(); bindProcessTaskModule(bind); + bindTaskPreferences(bind); }); diff --git a/packages/task/src/browser/task-preferences.ts b/packages/task/src/browser/task-preferences.ts new file mode 100644 index 0000000000000..789e68b756edc --- /dev/null +++ b/packages/task/src/browser/task-preferences.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * 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 { interfaces } from 'inversify'; +import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { taskSchemaId } from './task-schema-updater'; +import { TaskFolderPreferenceProvider } from './task-folder-preference-provider'; +import { FolderPreferenceProvider } from '@theia/preferences/lib/browser'; +import { PreferenceConfiguration } from '@theia/core/lib/browser/preferences/preference-configurations'; + +export const taskPreferencesSchema: PreferenceSchema = { + type: 'object', + scope: 'resource', + properties: { + tasks: { + $ref: taskSchemaId, + description: 'Task definition file', + defaultValue: { + tasks: [] + } + } + } +}; + +export function bindTaskPreferences(bind: interfaces.Bind): void { + bind(PreferenceContribution).toConstantValue({ schema: taskPreferencesSchema }); + bind(FolderPreferenceProvider).to(TaskFolderPreferenceProvider).inTransientScope().whenTargetNamed('tasks'); + bind(PreferenceConfiguration).toConstantValue({ name: 'tasks' }); +} diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index cec51a5a8b929..98b413e9003cd 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -20,6 +20,8 @@ import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import URI from '@theia/core/lib/common/uri'; import { TaskService } from './task-service'; +export const taskSchemaId = 'vscode://schemas/tasks'; + @injectable() export class TaskSchemaUpdater { @inject(JsonSchemaStore) @@ -45,15 +47,15 @@ export class TaskSchemaUpdater { }; const taskTypes = await this.taskService.getRegisteredTaskTypes(); taskSchema.properties.tasks.items.oneOf![0].allOf![0].properties!.type.enum = taskTypes; - const taskSchemaUrl = new URI('vscode://task/tasks.json'); + const taskSchemaUri = new URI(taskSchemaId); const contents = JSON.stringify(taskSchema); try { - this.inmemoryResources.update(taskSchemaUrl, contents); + this.inmemoryResources.update(taskSchemaUri, contents); } catch (e) { - this.inmemoryResources.add(taskSchemaUrl, contents); + this.inmemoryResources.add(taskSchemaUri, contents); this.jsonSchemaStore.registerSchema({ fileMatch: ['tasks.json'], - url: taskSchemaUrl.toString() + url: taskSchemaUri.toString() }); } } @@ -107,6 +109,7 @@ const commandOptionsSchema: IJSONSchema = { }; const taskConfigurationSchema: IJSONSchema = { + $id: taskSchemaId, oneOf: [ { allOf: [ diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index acfcf6b350255..0a06ba6cdcb3b 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -55,11 +55,6 @@ export interface QuickPickProblemMatcherItem { @injectable() export class TaskService implements TaskConfigurationClient { - /** - * Reflects whether a valid task configuration file was found - * in the current workspace, and is being watched for changes. - */ - protected configurationFileFound: boolean = false; /** * The last executed task. @@ -132,18 +127,6 @@ export class TaskService implements TaskConfigurationClient { @postConstruct() protected init(): void { - this.workspaceService.onWorkspaceChanged(async roots => { - this.configurationFileFound = (await Promise.all(roots.map(r => this.taskConfigurations.watchConfigurationFile(r.uri)))).some(result => !!result); - const rootUris = roots.map(r => new URI(r.uri)); - const taskConfigFileUris = this.taskConfigurations.configFileUris.map(strUri => new URI(strUri)); - for (const taskConfigUri of taskConfigFileUris) { - if (!rootUris.some(rootUri => !!rootUri.relative(taskConfigUri))) { - this.taskConfigurations.unwatchConfigurationFile(taskConfigUri.toString()); - this.taskConfigurations.removeTasks(taskConfigUri.toString()); - } - } - }); - // notify user that task has started this.taskWatcher.onTaskCreated((event: TaskInfo) => { if (this.isEventForThisClient(event.ctx)) {