From 3409b6965fa902a7c8dfea84a721dc6e748766ec Mon Sep 17 00:00:00 2001 From: Artem Zatsarynnyi Date: Mon, 20 Sep 2021 18:44:13 +0300 Subject: [PATCH] Track the changes in the workspace-specific configuration files Signed-off-by: Artem Zatsarynnyi --- .../eclipse-che-theia-workspace/package.json | 9 +- .../src/browser/che-workspace-module.ts | 17 ++ .../browser/workspace-config-files-watcher.ts | 171 ++++++++++++++++++ .../frontend-application-config-provider.ts} | 10 +- .../workspace-config-files-watcher.spec.ts | 106 +++++++++++ .../eclipse-che-theia-workspace/tests/mock.js | 19 ++ 6 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 extensions/eclipse-che-theia-workspace/src/browser/workspace-config-files-watcher.ts rename extensions/eclipse-che-theia-workspace/tests/{no-op.spec.ts => browser/frontend-application-config-provider.ts} (54%) create mode 100644 extensions/eclipse-che-theia-workspace/tests/browser/workspace-config-files-watcher.spec.ts create mode 100644 extensions/eclipse-che-theia-workspace/tests/mock.js diff --git a/extensions/eclipse-che-theia-workspace/package.json b/extensions/eclipse-che-theia-workspace/package.json index 47f578849..5713c7382 100644 --- a/extensions/eclipse-che-theia-workspace/package.json +++ b/extensions/eclipse-che-theia-workspace/package.json @@ -13,6 +13,7 @@ "@eclipse-che/api": "latest", "@theia/workspace": "next", "@eclipse-che/theia-remote-api": "^0.0.1", + "@eclipse-che/theia-plugin-ext": "^0.0.1", "js-yaml": "3.13.1" }, "devDependencies": { @@ -47,6 +48,12 @@ "modulePathIgnorePatterns": [ "/lib" ], - "preset": "ts-jest" + "preset": "ts-jest", + "moduleNameMapper": { + "\\.(css|less)$": "/tests/mock.js" + }, + "setupFilesAfterEnv": [ + "/tests/browser/frontend-application-config-provider.ts" + ] } } diff --git a/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts index 34b642bc0..3bd23b1f2 100644 --- a/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts +++ b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts @@ -12,6 +12,12 @@ import '../../src/browser/style/index.css'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; import { Container, ContainerModule, interfaces } from 'inversify'; +import { + DevfileWatcher, + ExtensionsJsonWatcher, + PluginsYamlWatcher, + TasksJsonWatcher, +} from './workspace-config-files-watcher'; import { FileTree, FileTreeModel, FileTreeWidget, createFileTreeContainer } from '@theia/filesystem/lib/browser'; import { FrontendApplicationContribution, @@ -48,6 +54,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).to(ExplorerContribution); rebind(FileNavigatorWidget).toDynamicValue(ctx => createFileNavigatorWidget(ctx.container)); + + const devWorkspaceName = process.env['DEVWORKSPACE_NAME']; + if (devWorkspaceName) { + bind(DevfileWatcher).toSelf().inSingletonScope(); + bind(ExtensionsJsonWatcher).toSelf().inSingletonScope(); + bind(PluginsYamlWatcher).toSelf().inSingletonScope(); + bind(TasksJsonWatcher).toSelf().inSingletonScope(); + [DevfileWatcher, ExtensionsJsonWatcher, PluginsYamlWatcher, TasksJsonWatcher].forEach(component => { + bind(FrontendApplicationContribution).to(component); + }); + } }); export function createFileNavigatorContainer(parent: interfaces.Container): Container { diff --git a/extensions/eclipse-che-theia-workspace/src/browser/workspace-config-files-watcher.ts b/extensions/eclipse-che-theia-workspace/src/browser/workspace-config-files-watcher.ts new file mode 100644 index 000000000..10a253582 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/browser/workspace-config-files-watcher.ts @@ -0,0 +1,171 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import * as jsYaml from 'js-yaml'; + +import { FileChangeType, FileChangesEvent } from '@theia/filesystem/lib/common/files'; +import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from 'inversify'; + +import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager'; +import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import URI from '@theia/core/lib/common/uri'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; + +import debounce = require('lodash.debounce'); + +/** + * Abstract watcher allows to track the changes in the project-specific configuration files. + * A concrete implementation can handle the changes in a specific way. + */ +@injectable() +export abstract class AbstractFileWatcher implements FrontendApplicationContribution { + @inject(FileService) + protected readonly fileService: FileService; + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + /** File name to watch, e.g. '.vscode/extensions.json'. */ + protected abstract fileName: string; + + /** + * Called when the frontend application is started. + */ + async onStart(app: FrontendApplication): Promise { + this.trackFilesInRoots(); + this.workspaceService.onWorkspaceChanged(() => this.trackFilesInRoots()); + } + + private async trackFilesInRoots(): Promise { + (await this.workspaceService.roots).forEach(root => { + const fileURI = root.resource.resolve(this.fileName); + this.fileService.watch(fileURI); + const onFileChange = async (event: FileChangesEvent) => { + if (event.contains(fileURI, FileChangeType.ADDED)) { + this.handleChange(fileURI, FileChangeType.ADDED); + } else if (event.contains(fileURI, FileChangeType.UPDATED)) { + this.handleChange(fileURI, FileChangeType.UPDATED); + } + }; + this.fileService.onDidFilesChange(debounce(onFileChange, 1000)); + }); + } + + /** + * Allows an implementor to handle a file change. + * + * @param fileURI an URI of the modified file + * @param changeType file change type + */ + protected abstract handleChange(fileURI: URI, changeType: FileChangeType): void; +} + +@injectable() +export class DevfileWatcher extends AbstractFileWatcher { + protected fileName = 'devfile.yaml'; + + @inject(DevfileService) + protected readonly devfileService: DevfileService; + + @inject(ChePluginManager) + protected readonly chePluginManager: ChePluginManager; + + @inject(MessageService) + protected readonly messageService: MessageService; + + protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise { + const message = + changeType === FileChangeType.ADDED + ? `A Devfile is found in ${fileURI}. Do you want to update your Workspace?` + : 'Do you want to update your Workspace with the changed Devfile?'; + const answer = await this.messageService.info(message, 'Yes', 'No'); + if (answer === 'Yes') { + this.updateWorkspaceWithDevfile(fileURI); + } + } + + /** + * Updates the workspace with the given Devfile. + * + * @param devfileURI URI of the Devfile to update the Workspace with + */ + protected async updateWorkspaceWithDevfile(devfileURI: URI): Promise { + const content = await this.fileService.readFile(devfileURI); + const devfile = jsYaml.load(content.value.toString()); + await this.devfileService.updateDevfile(devfile); + await this.chePluginManager.restartWorkspace(); + } +} + +@injectable() +export class ExtensionsJsonWatcher extends AbstractFileWatcher { + protected fileName = '.vscode/extensions.json'; + + @inject(ChePluginManager) + protected readonly chePluginManager: ChePluginManager; + + @inject(MessageService) + protected readonly messageService: MessageService; + + protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise { + const message = + changeType === FileChangeType.ADDED + ? `An extensions list is found in ${fileURI}. Do you want to update your Workspace with these extensions?` + : 'Do you want to update your Workspace with the changed "extensions.json"?'; + const answer = await this.messageService.info(message, 'Yes', 'No'); + if (answer === 'Yes') { + await this.chePluginManager.restartWorkspace(); + } + } +} + +@injectable() +export class PluginsYamlWatcher extends AbstractFileWatcher { + protected fileName = '.che/che-theia-plugins.yaml'; + + @inject(ChePluginManager) + protected readonly chePluginManager: ChePluginManager; + + @inject(MessageService) + protected readonly messageService: MessageService; + + protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise { + const message = + changeType === FileChangeType.ADDED + ? `A plug-ins list is found in ${fileURI}. Do you want to update your Workspace with these plug-ins?` + : 'Do you want to update your Workspace with the changed "che-theia-plugins.yaml"?'; + const answer = await this.messageService.info(message, 'Yes', 'No'); + if (answer === 'Yes') { + await this.chePluginManager.restartWorkspace(); + } + } +} + +@injectable() +export class TasksJsonWatcher extends AbstractFileWatcher { + protected fileName = '.vscode/tasks.json'; + + @inject(MessageService) + protected readonly messageService: MessageService; + + protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise { + const answer = await this.messageService.info( + 'Do you want to update your Workspace with the "tasks.json" changes?', + 'Yes', + 'No' + ); + if (answer === 'Yes') { + // TODO: set the tasks to the project's attributes + } + } +} diff --git a/extensions/eclipse-che-theia-workspace/tests/no-op.spec.ts b/extensions/eclipse-che-theia-workspace/tests/browser/frontend-application-config-provider.ts similarity index 54% rename from extensions/eclipse-che-theia-workspace/tests/no-op.spec.ts rename to extensions/eclipse-che-theia-workspace/tests/browser/frontend-application-config-provider.ts index 3ec8409a6..4e4fc190a 100644 --- a/extensions/eclipse-che-theia-workspace/tests/no-op.spec.ts +++ b/extensions/eclipse-che-theia-workspace/tests/browser/frontend-application-config-provider.ts @@ -8,6 +8,12 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ -describe('no-op', function () { - it('no-op', function () {}); +import 'reflect-metadata'; + +import { ApplicationProps } from '@theia/application-package/lib/application-props'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; + +FrontendApplicationConfigProvider.set({ + ...ApplicationProps.DEFAULT.frontend.config, + applicationName: 'test', }); diff --git a/extensions/eclipse-che-theia-workspace/tests/browser/workspace-config-files-watcher.spec.ts b/extensions/eclipse-che-theia-workspace/tests/browser/workspace-config-files-watcher.spec.ts new file mode 100644 index 000000000..a05a6a28f --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/tests/browser/workspace-config-files-watcher.spec.ts @@ -0,0 +1,106 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import 'reflect-metadata'; + +import { AbstractFileWatcher, DevfileWatcher } from '../../src/browser/workspace-config-files-watcher'; + +import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager'; +import { Container } from '@theia/core/shared/inversify'; +import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { FrontendApplication } from '@theia/core/lib/browser'; +import { MessageService } from '@theia/core'; +import URI from '@theia/core/lib/common/uri'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; + +describe('Test workspace config files watchers', function () { + let container: Container; + + let fileService: FileService; + let workspaceService: WorkspaceService; + + const fileServiceWatchMethod = jest.fn(); + const workspaceServiceOnWorkspaceChangedMethod = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + + container = new Container(); + + fileService = ({ + watch: fileServiceWatchMethod, + } as unknown) as FileService; + + workspaceService = ({ + onWorkspaceChanged: workspaceServiceOnWorkspaceChangedMethod, + } as unknown) as WorkspaceService; + + container.bind(FileService).toConstantValue(fileService); + container.bind(WorkspaceService).toConstantValue(workspaceService); + }); + + describe('Test DevfileWatcher', function () { + let devfileWatcher: AbstractFileWatcher; + + const devfileServiceUpdateDevfileMethod = jest.fn(); + const chePluginManagerRestartWorkspaceMethod = jest.fn(); + const messageServiceInfoMethod = jest.fn(); + + beforeEach(() => { + const devfileService = ({ + updateDevfile: devfileServiceUpdateDevfileMethod, + } as unknown) as DevfileService; + + const chePluginManager = ({ + restartWorkspace: chePluginManagerRestartWorkspaceMethod, + } as unknown) as ChePluginManager; + + const messageService = ({ + info: messageServiceInfoMethod, + } as unknown) as MessageService; + + container.bind(DevfileService).toConstantValue(devfileService); + container.bind(ChePluginManager).toConstantValue(chePluginManager); + container.bind(MessageService).toConstantValue(messageService); + container.bind(DevfileWatcher).toSelf().inSingletonScope(); + devfileWatcher = container.get(DevfileWatcher); + }); + + test('shouldWatch', async () => { + const resolveFn = jest.fn(); + const resource = ({ + resolve: resolveFn, + } as unknown) as URI; + resolveFn.mockReturnValue(resource); + + const roots: FileStat[] = [ + { + name: 'testFile', + isDirectory: false, + isFile: true, + isSymbolicLink: false, + resource: resource, + }, + ]; + + Object.defineProperty(workspaceService, 'roots', { + get: jest.fn(() => roots), + }); + + await devfileWatcher.onStart({} as FrontendApplication); + + expect(fileServiceWatchMethod.mock.calls.length).toEqual(1); + expect(fileServiceWatchMethod).toHaveBeenCalledWith(resource); + }); + }); +}); diff --git a/extensions/eclipse-che-theia-workspace/tests/mock.js b/extensions/eclipse-che-theia-workspace/tests/mock.js new file mode 100644 index 000000000..43b9cb768 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/tests/mock.js @@ -0,0 +1,19 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 + ********************************************************************************/ + +'use strict'; + +module.exports = {};