// ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as jsoncparser from 'jsonc-parser'; import debounce = require('p-debounce'); import { inject, injectable, postConstruct } from '@theia/core/shared/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 { PreferenceScope, PreferenceService, DisposableCollection, PreferenceProviderProvider, nls } from '@theia/core/lib/common'; 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 { TaskTemplateSelector } from './task-templates'; import { TaskCustomization, TaskConfiguration, TaskConfigurationScope, TaskScope } from '../common/task-protocol'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { PreferenceConfigurations } from '@theia/core/lib/common/preferences/preference-configurations'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { TaskSchemaUpdater } from './task-schema-updater'; import { JSONObject } from '@theia/core/shared/@lumino/coreutils'; import { PreferenceProvider } from '@theia/core/lib/common/preferences/preference-provider'; export interface TasksChange { scope: TaskConfigurationScope; type: FileChangeType; } /** * This class connects the the "tasks" preferences sections to task system: it collects tasks preference values and * provides them to the task system as raw, parsed JSON. */ @injectable() export class TaskConfigurationManager { @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(QuickPickService) protected readonly quickPickService: QuickPickService; @inject(FileService) protected readonly fileService: FileService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @inject(TaskSchemaUpdater) protected readonly taskSchemaProvider: TaskSchemaUpdater; @inject(PreferenceProviderProvider) protected readonly preferenceProviderProvider: PreferenceProviderProvider; @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations; @inject(WorkspaceVariableContribution) protected readonly workspaceVariables: WorkspaceVariableContribution; @inject(TaskTemplateSelector) protected readonly taskTemplateSelector: TaskTemplateSelector; protected readonly onDidChangeTaskConfigEmitter = new Emitter(); readonly onDidChangeTaskConfig: Event = this.onDidChangeTaskConfigEmitter.event; protected readonly models = new Map(); protected workspaceDelegate: PreferenceProvider | undefined; @postConstruct() protected init(): void { this.createModels(); this.preferenceProviderProvider(PreferenceScope.Folder)?.onDidPreferencesChanged(e => { if (e['tasks']) { this.updateModels(); } }); this.workspaceService.onWorkspaceChanged(() => { this.updateModels(); }); this.workspaceService.onWorkspaceLocationChanged(() => { this.updateModels(); }); } protected createModels(): void { const userModel = new TaskConfigurationModel(TaskScope.Global, this.preferenceProviderProvider(PreferenceScope.User)); 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()] .filter(key => key !== TaskScope.Global && key !== TaskScope.Workspace) ); this.updateWorkspaceModel(); for (const rootStat of roots) { const key = rootStat.resource.toString(); toDelete.delete(key); if (!this.models.has(key)) { const model = new TaskConfigurationModel(key, this.preferenceProviderProvider(PreferenceScope.Folder)); model.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED })); model.onDispose(() => this.models.delete(key)); this.models.set(key, model); this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED }); } } for (const uri of toDelete) { const model = this.models.get(uri); if (model) { model.dispose(); } this.onDidChangeTaskConfigEmitter.fire({ scope: uri, type: FileChangeType.DELETED }); } }, 500); getTasks(scope: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] { return this.getModel(scope)?.configurations ?? []; } getTask(name: string, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration | undefined { return this.getTasks(scope).find((configuration: TaskCustomization | TaskConfiguration) => configuration.name === name); } async openConfiguration(scope: TaskConfigurationScope): Promise { const taskPrefModel = this.getModel(scope); const maybeURI = typeof scope === 'string' ? scope : undefined; const configURI = this.preferenceService.getConfigUri(this.getMatchingPreferenceScope(scope), maybeURI, 'tasks'); if (taskPrefModel && configURI) { await this.doOpen(taskPrefModel, configURI); } } async addTaskConfiguration(scope: TaskConfigurationScope, taskConfig: TaskCustomization): Promise { const taskPrefModel = this.getModel(scope); if (taskPrefModel) { const configurations = taskPrefModel.configurations; return this.setTaskConfigurations(scope, [...configurations, taskConfig]); } return false; } async setTaskConfigurations(scope: TaskConfigurationScope, taskConfigs: (TaskCustomization | TaskConfiguration)[]): Promise { const taskPrefModel = this.getModel(scope); if (taskPrefModel) { return taskPrefModel.setConfigurations(taskConfigs); } return false; } protected getModel(scope: TaskConfigurationScope): TaskConfigurationModel | undefined { return this.models.get(scope); } protected async doOpen(model: TaskConfigurationModel, configURI: URI): Promise { if (!model.uri) { // The file has not yet been created. await this.doCreate(model, configURI); } return this.editorManager.open(configURI, { mode: 'activate' }); } protected async doCreate(model: TaskConfigurationModel, configURI: URI): Promise { const content = await this.getInitialConfigurationContent(); if (content) { // All scopes but workspace. if (this.preferenceConfigurations.getName(configURI) === 'tasks') { await this.fileService.write(configURI, content); } else { let taskContent: object; try { taskContent = jsoncparser.parse(content); } catch { taskContent = this.taskSchemaProvider.getTaskSchema().default ?? {}; } await model.preferences?.setPreference('tasks', taskContent as JSONObject); } } } protected getMatchingPreferenceScope(scope: TaskConfigurationScope): PreferenceScope { switch (scope) { case TaskScope.Global: return PreferenceScope.User; case TaskScope.Workspace: return PreferenceScope.Workspace; default: return PreferenceScope.Folder; } } protected async getInitialConfigurationContent(): Promise { const selected = await this.quickPickService.show(this.taskTemplateSelector.selectTemplates(), { placeholder: nls.localizeByDefault('Select a Task Template') }); if (selected) { return selected.value?.content; } } protected readonly toDisposeOnDelegateChange = new DisposableCollection(); protected updateWorkspaceModel(): void { const isFolderWorkspace = this.workspaceService.opened && !this.workspaceService.saved; const newDelegate = isFolderWorkspace ? this.preferenceProviderProvider(PreferenceScope.Folder) : this.preferenceProviderProvider(PreferenceScope.Workspace); const effectiveScope = isFolderWorkspace ? this.workspaceService.tryGetRoots()[0]?.resource.toString() : TaskScope.Workspace; 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.preferenceProviderProvider(PreferenceScope.Workspace)) { this.toDisposeOnDelegateChange.push(workspaceModel.onDidChange(() => { this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Workspace, type: FileChangeType.UPDATED }); })); } this.models.set(TaskScope.Workspace, workspaceModel); this.onDidChangeTaskConfigEmitter.fire({ scope: effectiveScope, type: FileChangeType.UPDATED }); } } } export namespace TaskConfigurationManager { export interface Data { current?: { name: string workspaceFolderUri?: string } } }