Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
Track the changes in the workspace-specific configuration files
Browse files Browse the repository at this point in the history
Signed-off-by: Artem Zatsarynnyi <[email protected]>
  • Loading branch information
azatsarynnyy committed Sep 20, 2021
1 parent f85127c commit 3409b69
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 3 deletions.
9 changes: 8 additions & 1 deletion extensions/eclipse-che-theia-workspace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -47,6 +48,12 @@
"modulePathIgnorePatterns": [
"<rootDir>/lib"
],
"preset": "ts-jest"
"preset": "ts-jest",
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/tests/mock.js"
},
"setupFilesAfterEnv": [
"<rootDir>/tests/browser/frontend-application-config-provider.ts"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
this.trackFilesInRoots();
this.workspaceService.onWorkspaceChanged(() => this.trackFilesInRoots());
}

private async trackFilesInRoots(): Promise<void> {
(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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
19 changes: 19 additions & 0 deletions extensions/eclipse-che-theia-workspace/tests/mock.js
Original file line number Diff line number Diff line change
@@ -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 = {};

0 comments on commit 3409b69

Please sign in to comment.