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

Commit 3409b69

Browse files
committed
Track the changes in the workspace-specific configuration files
Signed-off-by: Artem Zatsarynnyi <[email protected]>
1 parent f85127c commit 3409b69

File tree

6 files changed

+329
-3
lines changed

6 files changed

+329
-3
lines changed

extensions/eclipse-che-theia-workspace/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@eclipse-che/api": "latest",
1414
"@theia/workspace": "next",
1515
"@eclipse-che/theia-remote-api": "^0.0.1",
16+
"@eclipse-che/theia-plugin-ext": "^0.0.1",
1617
"js-yaml": "3.13.1"
1718
},
1819
"devDependencies": {
@@ -47,6 +48,12 @@
4748
"modulePathIgnorePatterns": [
4849
"<rootDir>/lib"
4950
],
50-
"preset": "ts-jest"
51+
"preset": "ts-jest",
52+
"moduleNameMapper": {
53+
"\\.(css|less)$": "<rootDir>/tests/mock.js"
54+
},
55+
"setupFilesAfterEnv": [
56+
"<rootDir>/tests/browser/frontend-application-config-provider.ts"
57+
]
5158
}
5259
}

extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import '../../src/browser/style/index.css';
1212

1313
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
1414
import { Container, ContainerModule, interfaces } from 'inversify';
15+
import {
16+
DevfileWatcher,
17+
ExtensionsJsonWatcher,
18+
PluginsYamlWatcher,
19+
TasksJsonWatcher,
20+
} from './workspace-config-files-watcher';
1521
import { FileTree, FileTreeModel, FileTreeWidget, createFileTreeContainer } from '@theia/filesystem/lib/browser';
1622
import {
1723
FrontendApplicationContribution,
@@ -48,6 +54,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
4854
bind(FrontendApplicationContribution).to(ExplorerContribution);
4955

5056
rebind(FileNavigatorWidget).toDynamicValue(ctx => createFileNavigatorWidget(ctx.container));
57+
58+
const devWorkspaceName = process.env['DEVWORKSPACE_NAME'];
59+
if (devWorkspaceName) {
60+
bind(DevfileWatcher).toSelf().inSingletonScope();
61+
bind(ExtensionsJsonWatcher).toSelf().inSingletonScope();
62+
bind(PluginsYamlWatcher).toSelf().inSingletonScope();
63+
bind(TasksJsonWatcher).toSelf().inSingletonScope();
64+
[DevfileWatcher, ExtensionsJsonWatcher, PluginsYamlWatcher, TasksJsonWatcher].forEach(component => {
65+
bind(FrontendApplicationContribution).to(component);
66+
});
67+
}
5168
});
5269

5370
export function createFileNavigatorContainer(parent: interfaces.Container): Container {
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**********************************************************************
2+
* Copyright (c) 2021 Red Hat, Inc.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
***********************************************************************/
10+
11+
import * as jsYaml from 'js-yaml';
12+
13+
import { FileChangeType, FileChangesEvent } from '@theia/filesystem/lib/common/files';
14+
import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
15+
import { inject, injectable } from 'inversify';
16+
17+
import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager';
18+
import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service';
19+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
20+
import { MessageService } from '@theia/core/lib/common/message-service';
21+
import URI from '@theia/core/lib/common/uri';
22+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
23+
24+
import debounce = require('lodash.debounce');
25+
26+
/**
27+
* Abstract watcher allows to track the changes in the project-specific configuration files.
28+
* A concrete implementation can handle the changes in a specific way.
29+
*/
30+
@injectable()
31+
export abstract class AbstractFileWatcher implements FrontendApplicationContribution {
32+
@inject(FileService)
33+
protected readonly fileService: FileService;
34+
35+
@inject(WorkspaceService)
36+
protected readonly workspaceService: WorkspaceService;
37+
38+
/** File name to watch, e.g. '.vscode/extensions.json'. */
39+
protected abstract fileName: string;
40+
41+
/**
42+
* Called when the frontend application is started.
43+
*/
44+
async onStart(app: FrontendApplication): Promise<void> {
45+
this.trackFilesInRoots();
46+
this.workspaceService.onWorkspaceChanged(() => this.trackFilesInRoots());
47+
}
48+
49+
private async trackFilesInRoots(): Promise<void> {
50+
(await this.workspaceService.roots).forEach(root => {
51+
const fileURI = root.resource.resolve(this.fileName);
52+
this.fileService.watch(fileURI);
53+
const onFileChange = async (event: FileChangesEvent) => {
54+
if (event.contains(fileURI, FileChangeType.ADDED)) {
55+
this.handleChange(fileURI, FileChangeType.ADDED);
56+
} else if (event.contains(fileURI, FileChangeType.UPDATED)) {
57+
this.handleChange(fileURI, FileChangeType.UPDATED);
58+
}
59+
};
60+
this.fileService.onDidFilesChange(debounce(onFileChange, 1000));
61+
});
62+
}
63+
64+
/**
65+
* Allows an implementor to handle a file change.
66+
*
67+
* @param fileURI an URI of the modified file
68+
* @param changeType file change type
69+
*/
70+
protected abstract handleChange(fileURI: URI, changeType: FileChangeType): void;
71+
}
72+
73+
@injectable()
74+
export class DevfileWatcher extends AbstractFileWatcher {
75+
protected fileName = 'devfile.yaml';
76+
77+
@inject(DevfileService)
78+
protected readonly devfileService: DevfileService;
79+
80+
@inject(ChePluginManager)
81+
protected readonly chePluginManager: ChePluginManager;
82+
83+
@inject(MessageService)
84+
protected readonly messageService: MessageService;
85+
86+
protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
87+
const message =
88+
changeType === FileChangeType.ADDED
89+
? `A Devfile is found in ${fileURI}. Do you want to update your Workspace?`
90+
: 'Do you want to update your Workspace with the changed Devfile?';
91+
const answer = await this.messageService.info(message, 'Yes', 'No');
92+
if (answer === 'Yes') {
93+
this.updateWorkspaceWithDevfile(fileURI);
94+
}
95+
}
96+
97+
/**
98+
* Updates the workspace with the given Devfile.
99+
*
100+
* @param devfileURI URI of the Devfile to update the Workspace with
101+
*/
102+
protected async updateWorkspaceWithDevfile(devfileURI: URI): Promise<void> {
103+
const content = await this.fileService.readFile(devfileURI);
104+
const devfile = jsYaml.load(content.value.toString());
105+
await this.devfileService.updateDevfile(devfile);
106+
await this.chePluginManager.restartWorkspace();
107+
}
108+
}
109+
110+
@injectable()
111+
export class ExtensionsJsonWatcher extends AbstractFileWatcher {
112+
protected fileName = '.vscode/extensions.json';
113+
114+
@inject(ChePluginManager)
115+
protected readonly chePluginManager: ChePluginManager;
116+
117+
@inject(MessageService)
118+
protected readonly messageService: MessageService;
119+
120+
protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
121+
const message =
122+
changeType === FileChangeType.ADDED
123+
? `An extensions list is found in ${fileURI}. Do you want to update your Workspace with these extensions?`
124+
: 'Do you want to update your Workspace with the changed "extensions.json"?';
125+
const answer = await this.messageService.info(message, 'Yes', 'No');
126+
if (answer === 'Yes') {
127+
await this.chePluginManager.restartWorkspace();
128+
}
129+
}
130+
}
131+
132+
@injectable()
133+
export class PluginsYamlWatcher extends AbstractFileWatcher {
134+
protected fileName = '.che/che-theia-plugins.yaml';
135+
136+
@inject(ChePluginManager)
137+
protected readonly chePluginManager: ChePluginManager;
138+
139+
@inject(MessageService)
140+
protected readonly messageService: MessageService;
141+
142+
protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
143+
const message =
144+
changeType === FileChangeType.ADDED
145+
? `A plug-ins list is found in ${fileURI}. Do you want to update your Workspace with these plug-ins?`
146+
: 'Do you want to update your Workspace with the changed "che-theia-plugins.yaml"?';
147+
const answer = await this.messageService.info(message, 'Yes', 'No');
148+
if (answer === 'Yes') {
149+
await this.chePluginManager.restartWorkspace();
150+
}
151+
}
152+
}
153+
154+
@injectable()
155+
export class TasksJsonWatcher extends AbstractFileWatcher {
156+
protected fileName = '.vscode/tasks.json';
157+
158+
@inject(MessageService)
159+
protected readonly messageService: MessageService;
160+
161+
protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
162+
const answer = await this.messageService.info(
163+
'Do you want to update your Workspace with the "tasks.json" changes?',
164+
'Yes',
165+
'No'
166+
);
167+
if (answer === 'Yes') {
168+
// TODO: set the tasks to the project's attributes
169+
}
170+
}
171+
}

extensions/eclipse-che-theia-workspace/tests/no-op.spec.ts renamed to extensions/eclipse-che-theia-workspace/tests/browser/frontend-application-config-provider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
* SPDX-License-Identifier: EPL-2.0
99
***********************************************************************/
1010

11-
describe('no-op', function () {
12-
it('no-op', function () {});
11+
import 'reflect-metadata';
12+
13+
import { ApplicationProps } from '@theia/application-package/lib/application-props';
14+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
15+
16+
FrontendApplicationConfigProvider.set({
17+
...ApplicationProps.DEFAULT.frontend.config,
18+
applicationName: 'test',
1319
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**********************************************************************
2+
* Copyright (c) 2021 Red Hat, Inc.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
***********************************************************************/
10+
11+
import 'reflect-metadata';
12+
13+
import { AbstractFileWatcher, DevfileWatcher } from '../../src/browser/workspace-config-files-watcher';
14+
15+
import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager';
16+
import { Container } from '@theia/core/shared/inversify';
17+
import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service';
18+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
19+
import { FileStat } from '@theia/filesystem/lib/common/files';
20+
import { FrontendApplication } from '@theia/core/lib/browser';
21+
import { MessageService } from '@theia/core';
22+
import URI from '@theia/core/lib/common/uri';
23+
import { WorkspaceService } from '@theia/workspace/lib/browser';
24+
25+
describe('Test workspace config files watchers', function () {
26+
let container: Container;
27+
28+
let fileService: FileService;
29+
let workspaceService: WorkspaceService;
30+
31+
const fileServiceWatchMethod = jest.fn();
32+
const workspaceServiceOnWorkspaceChangedMethod = jest.fn();
33+
34+
beforeEach(() => {
35+
jest.restoreAllMocks();
36+
jest.resetAllMocks();
37+
38+
container = new Container();
39+
40+
fileService = ({
41+
watch: fileServiceWatchMethod,
42+
} as unknown) as FileService;
43+
44+
workspaceService = ({
45+
onWorkspaceChanged: workspaceServiceOnWorkspaceChangedMethod,
46+
} as unknown) as WorkspaceService;
47+
48+
container.bind(FileService).toConstantValue(fileService);
49+
container.bind(WorkspaceService).toConstantValue(workspaceService);
50+
});
51+
52+
describe('Test DevfileWatcher', function () {
53+
let devfileWatcher: AbstractFileWatcher;
54+
55+
const devfileServiceUpdateDevfileMethod = jest.fn();
56+
const chePluginManagerRestartWorkspaceMethod = jest.fn();
57+
const messageServiceInfoMethod = jest.fn();
58+
59+
beforeEach(() => {
60+
const devfileService = ({
61+
updateDevfile: devfileServiceUpdateDevfileMethod,
62+
} as unknown) as DevfileService;
63+
64+
const chePluginManager = ({
65+
restartWorkspace: chePluginManagerRestartWorkspaceMethod,
66+
} as unknown) as ChePluginManager;
67+
68+
const messageService = ({
69+
info: messageServiceInfoMethod,
70+
} as unknown) as MessageService;
71+
72+
container.bind(DevfileService).toConstantValue(devfileService);
73+
container.bind(ChePluginManager).toConstantValue(chePluginManager);
74+
container.bind(MessageService).toConstantValue(messageService);
75+
container.bind(DevfileWatcher).toSelf().inSingletonScope();
76+
devfileWatcher = container.get(DevfileWatcher);
77+
});
78+
79+
test('shouldWatch', async () => {
80+
const resolveFn = jest.fn();
81+
const resource = ({
82+
resolve: resolveFn,
83+
} as unknown) as URI;
84+
resolveFn.mockReturnValue(resource);
85+
86+
const roots: FileStat[] = [
87+
{
88+
name: 'testFile',
89+
isDirectory: false,
90+
isFile: true,
91+
isSymbolicLink: false,
92+
resource: resource,
93+
},
94+
];
95+
96+
Object.defineProperty(workspaceService, 'roots', {
97+
get: jest.fn(() => roots),
98+
});
99+
100+
await devfileWatcher.onStart({} as FrontendApplication);
101+
102+
expect(fileServiceWatchMethod.mock.calls.length).toEqual(1);
103+
expect(fileServiceWatchMethod).toHaveBeenCalledWith(resource);
104+
});
105+
});
106+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/********************************************************************************
2+
* Copyright (C) 2021 Red Hat, Inc. and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
'use strict';
18+
19+
module.exports = {};

0 commit comments

Comments
 (0)