diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-debug-configuration-manager.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-debug-configuration-manager.ts new file mode 100644 index 000000000..00bc99e9f --- /dev/null +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-debug-configuration-manager.ts @@ -0,0 +1,32 @@ +/********************************************************************** + * 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 { injectable, postConstruct } from 'inversify'; + +import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; + +@injectable() +export class CheDebugConfigurationManager extends DebugConfigurationManager { + @postConstruct() + protected async init(): Promise { + super.init(); + + /** + * Theia creates a DebugConfigurationModel for each workspace folder in a workspace at starting the IDE. + * For the CHE multi-root workspace there no workspace folders at that step: + * CHE clones projects at starting the IDE and adds a workspace folder directly after cloning a project. + * That's why we need the following logic - + * DebugConfigurationManager should create the corresponding model when a workspace is changed (a workspace folder is added) + */ + this.workspaceService.onWorkspaceChanged(() => { + this.updateModels(); + }); + } +} diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts index 4e33bcf3d..aa9fa0d62 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts @@ -22,9 +22,11 @@ import { } from '../common/che-protocol'; import { CheSideCarContentReaderRegistryImpl, CheSideCarResourceResolver } from './che-sidecar-resource'; import { CommandContribution, ResourceResolver } from '@theia/core/lib/common'; +import { ContainerModule, interfaces } from 'inversify'; import { WebSocketConnectionProvider, WidgetFactory } from '@theia/core/lib/browser'; import { CheApiProvider } from './che-api-provider'; +import { CheDebugConfigurationManager } from './che-debug-configuration-manager'; import { CheLanguagesMainTestImpl } from './che-languages-test-main'; import { ChePluginCommandContribution } from './plugin/che-plugin-command-contribution'; import { ChePluginFrontentService } from './plugin/che-plugin-frontend-service'; @@ -38,8 +40,8 @@ import { CheTaskClientImpl } from './che-task-client'; import { CheTaskResolver } from './che-task-resolver'; import { CheTaskTerminalWidgetManager } from './che-task-terminal-widget-manager'; import { CheWebviewEnvironment } from './che-webview-environment'; -import { ContainerModule } from 'inversify'; import { ContainerPicker } from './container-picker'; +import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; import { LanguagesMainFactory } from '@theia/plugin-ext'; import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution'; import { PluginFrontendViewContribution } from '@theia/plugin-ext/lib/main/browser/plugin-frontend-view-contribution'; @@ -50,7 +52,6 @@ import { TaskStatusHandler } from './task-status-handler'; import { TaskTerminalWidgetManager } from '@theia/task/lib/browser/task-terminal-widget-manager'; import { WebviewEnvironment } from '@theia/plugin-ext/lib/main/browser/webview/webview-environment'; import { bindChePluginPreferences } from './plugin/che-plugin-preferences'; -import { interfaces } from 'inversify'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(CheApiProvider).toSelf().inSingletonScope(); @@ -124,4 +125,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { child.bind(RPCProtocol).toConstantValue(rpc); return child.get(CheLanguagesMainTestImpl); }); + + bind(CheDebugConfigurationManager).toSelf().inSingletonScope(); + rebind(DebugConfigurationManager).to(CheDebugConfigurationManager).inSingletonScope(); }); diff --git a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts index f54e36c7f..4b041e654 100644 --- a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts +++ b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts @@ -46,12 +46,22 @@ import pluginExtBackendModule from '@theia/plugin-ext/lib/plugin-ext-backend-mod import pluginRemoteBackendModule from './plugin-remote-backend-module'; import pluginVscodeBackendModule from '@theia/plugin-ext-vscode/lib/node/plugin-vscode-backend-module'; +const DEFAULT_THEIA_HOME_DIR = '/home/theia/'; +const DEFAULT_THEIA_DEV_HOME_DIR = '/home/theia-dev'; + interface CheckAliveWS extends ws { alive: boolean; } -function modifyPathToLocal(origPath: string): string { - return path.join(os.homedir(), origPath.substr(0, '/home/theia/'.length)); +function modifyPathToLocal(originalPath: string): string { + if (originalPath.startsWith(DEFAULT_THEIA_HOME_DIR)) { + return path.join(os.homedir(), originalPath.substring(DEFAULT_THEIA_HOME_DIR.length)); + } + + if (originalPath.startsWith(DEFAULT_THEIA_DEV_HOME_DIR)) { + return path.join(os.homedir(), originalPath.substring(DEFAULT_THEIA_DEV_HOME_DIR.length)); + } + return originalPath; } @injectable() @@ -127,10 +137,22 @@ export class PluginRemoteInit { const originalStart = PluginManagerExtImpl.prototype.$start; PluginManagerExtImpl.prototype.$start = async function (params: PluginManagerStartParams): Promise { const { hostLogPath, hostStoragePath, hostGlobalStoragePath } = params.configStorage; + + const overriddenLogPath = modifyPathToLocal(hostLogPath); + await fs.ensureDir(overriddenLogPath); + + const overriddenStoragePath = hostStoragePath ? modifyPathToLocal(hostStoragePath) : undefined; + if (overriddenStoragePath) { + await fs.ensureDir(overriddenStoragePath); + } + + const overriddenGlobalStoragePath = modifyPathToLocal(hostGlobalStoragePath); + await fs.ensureDir(overriddenGlobalStoragePath); + params.configStorage = { - hostLogPath: modifyPathToLocal(hostLogPath), - hostStoragePath: hostStoragePath ? modifyPathToLocal(hostStoragePath) : undefined, - hostGlobalStoragePath: modifyPathToLocal(hostGlobalStoragePath), + hostLogPath: overriddenLogPath, + hostStoragePath: overriddenStoragePath, + hostGlobalStoragePath: overriddenGlobalStoragePath, }; // call original method return originalStart.call(this, params); diff --git a/extensions/eclipse-che-theia-workspace/package.json b/extensions/eclipse-che-theia-workspace/package.json index 8cae0a0e0..833d94f12 100644 --- a/extensions/eclipse-che-theia-workspace/package.json +++ b/extensions/eclipse-che-theia-workspace/package.json @@ -9,6 +9,7 @@ "src" ], "dependencies": { + "@theia/core": "next", "@eclipse-che/api": "latest", "@theia/workspace": "next", "@eclipse-che/theia-remote-api": "^0.0.1", @@ -31,7 +32,8 @@ "license": "EPL-2.0", "theiaExtensions": [ { - "frontend": "lib/browser/che-workspace-module" + "frontend": "lib/browser/che-workspace-module", + "backend": "lib/node/workspace-backend-module" } ] } diff --git a/extensions/eclipse-che-theia-workspace/src/browser/che-navigator-widget.tsx b/extensions/eclipse-che-theia-workspace/src/browser/che-navigator-widget.tsx new file mode 100644 index 000000000..832802dd7 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/browser/che-navigator-widget.tsx @@ -0,0 +1,26 @@ +/********************************************************************** + * 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 React from 'react'; + +import { injectable } from 'inversify'; + +import { FileNavigatorWidget } from '@theia/navigator/lib/browser/navigator-widget'; + +@injectable() +export class CheFileNavigatorWidget extends FileNavigatorWidget { + protected renderEmptyMultiRootWorkspace(): React.ReactNode { + return ( +
+
No projects in the workspace yet
+
+ ); + } +} 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 4e5cfa690..84cdca7b8 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 @@ -7,15 +7,31 @@ * * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ - import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; +import { Container, ContainerModule, interfaces } from 'inversify'; +import { FileTree, FileTreeModel, FileTreeWidget, createFileTreeContainer } from '@theia/filesystem/lib/browser'; +import { + FrontendApplicationContribution, + Tree, + TreeDecoratorService, + TreeModel, + TreeProps, +} from '@theia/core/lib/browser'; +import { + NavigatorDecoratorService, + NavigatorTreeDecorator, +} from '@theia/navigator/lib/browser/navigator-decorator-service'; +import { CheFileNavigatorWidget } from './che-navigator-widget'; import { CheWorkspaceContribution } from './che-workspace-contribution'; import { CheWorkspaceController } from './che-workspace-controller'; -import { ContainerModule } from 'inversify'; import { ExplorerContribution } from './explorer-contribution'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { FILE_NAVIGATOR_PROPS } from '@theia/navigator/lib/browser/navigator-container'; +import { FileNavigatorModel } from '@theia/navigator/lib/browser/navigator-model'; +import { FileNavigatorTree } from '@theia/navigator/lib/browser/navigator-tree'; +import { FileNavigatorWidget } from '@theia/navigator/lib/browser/navigator-widget'; import { QuickOpenCheWorkspace } from './che-quick-open-workspace'; +import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(QuickOpenCheWorkspace).toSelf().inSingletonScope(); @@ -27,4 +43,33 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ExplorerContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).to(ExplorerContribution); + + rebind(FileNavigatorWidget).toDynamicValue(ctx => createFileNavigatorWidget(ctx.container)); }); + +export function createFileNavigatorContainer(parent: interfaces.Container): Container { + const child = createFileTreeContainer(parent); + + child.unbind(FileTree); + child.bind(FileNavigatorTree).toSelf(); + child.rebind(Tree).toService(FileNavigatorTree); + + child.unbind(FileTreeModel); + child.bind(FileNavigatorModel).toSelf(); + child.rebind(TreeModel).toService(FileNavigatorModel); + + child.unbind(FileTreeWidget); + child.bind(CheFileNavigatorWidget).toSelf(); + + child.rebind(TreeProps).toConstantValue(FILE_NAVIGATOR_PROPS); + + child.bind(NavigatorDecoratorService).toSelf().inSingletonScope(); + child.rebind(TreeDecoratorService).toService(NavigatorDecoratorService); + bindContributionProvider(child, NavigatorTreeDecorator); + + return child; +} + +export function createFileNavigatorWidget(parent: interfaces.Container): CheFileNavigatorWidget { + return createFileNavigatorContainer(parent).get(CheFileNavigatorWidget); +} diff --git a/extensions/eclipse-che-theia-workspace/src/node/che-workspace-server.ts b/extensions/eclipse-che-theia-workspace/src/node/che-workspace-server.ts new file mode 100644 index 000000000..e9e8506f0 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/node/che-workspace-server.ts @@ -0,0 +1,61 @@ +/********************************************************************** + * 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 fs from 'fs-extra'; +import * as path from 'path'; + +import { Workspace, WorkspaceService } from '@eclipse-che/theia-remote-api/lib/common/workspace-service'; +import { inject, injectable } from 'inversify'; + +import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; +import { FileUri } from '@theia/core/lib/node'; + +interface TheiaWorkspace { + folders: TheiaWorkspacePath[]; +} + +interface TheiaWorkspacePath { + path: string; +} + +@injectable() +export class CheWorkspaceServer extends DefaultWorkspaceServer { + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + // override any workspace that could have been defined through CLI and use entries from the devfile + // if not possible, use default method + protected async getRoot(): Promise { + const workspace = await this.workspaceService.currentWorkspace(); + if (!isMultiRoot(workspace)) { + return super.getRoot(); + } + + const projectsRootEnvVariable = process.env.CHE_PROJECTS_ROOT; + const projectsRoot = projectsRootEnvVariable ? projectsRootEnvVariable : '/projects'; + + // first, check if we have a che.theia-workspace file + const cheTheiaWorkspaceFile = path.resolve(projectsRoot, 'che.theia-workspace'); + const cheTheiaWorkspaceFileUri = FileUri.create(cheTheiaWorkspaceFile); + const exists = await fs.pathExists(cheTheiaWorkspaceFile); + if (!exists) { + // no, then create the file + const theiaWorkspace: TheiaWorkspace = { folders: [] }; + await fs.writeFile(cheTheiaWorkspaceFile, JSON.stringify(theiaWorkspace), { encoding: 'utf8' }); + } + + return cheTheiaWorkspaceFileUri.toString(); + } +} + +function isMultiRoot(workspace: Workspace): boolean { + const devfile = workspace.devfile; + return !!devfile && !!devfile.attributes && !!devfile.attributes.multiRoot && devfile.attributes.multiRoot === 'on'; +} diff --git a/extensions/eclipse-che-theia-workspace/src/node/index.ts b/extensions/eclipse-che-theia-workspace/src/node/index.ts new file mode 100644 index 000000000..fb9c9d431 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/node/index.ts @@ -0,0 +1,11 @@ +/********************************************************************** + * 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 + ***********************************************************************/ + +export * from './workspace-backend-module'; diff --git a/extensions/eclipse-che-theia-workspace/src/node/workspace-backend-module.ts b/extensions/eclipse-che-theia-workspace/src/node/workspace-backend-module.ts new file mode 100644 index 000000000..4a3d6a2bb --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/node/workspace-backend-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * 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 { CheWorkspaceServer } from './che-workspace-server'; +import { ContainerModule } from 'inversify'; +import { WorkspaceServer } from '@theia/workspace/lib/common'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(CheWorkspaceServer).toSelf().inSingletonScope(); + rebind(WorkspaceServer).toService(CheWorkspaceServer); +}); diff --git a/plugins/task-plugin/package.json b/plugins/task-plugin/package.json index d397dbdda..481bf622b 100644 --- a/plugins/task-plugin/package.json +++ b/plugins/task-plugin/package.json @@ -70,6 +70,7 @@ "vscode-uri": "2.1.1", "vscode-ws-jsonrpc": "0.2.0", "ws": "^5.2.2", + "fs-extra": "^8.1.0", "jsonc-parser": "^2.0.2" } } diff --git a/plugins/task-plugin/src/che-task-backend-module.ts b/plugins/task-plugin/src/che-task-backend-module.ts index 1a592d70d..b964bcc77 100644 --- a/plugins/task-plugin/src/che-task-backend-module.ts +++ b/plugins/task-plugin/src/che-task-backend-module.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -49,6 +49,7 @@ container.bind(ProjectPathVariableResolver).toSelf().inSingletonScope(); container.bind(CheWorkspaceClient).toSelf().inSingletonScope(); container.bind(CheTaskPreviewMode).toSelf().inSingletonScope(); container.bind(PreviewUrlOpenService).toSelf().inSingletonScope(); +container.bind(LaunchConfigurationsExporter).toSelf().inSingletonScope(); container.bind(ConfigurationsExporter).to(TaskConfigurationsExporter).inSingletonScope(); container.bind(ConfigurationsExporter).to(LaunchConfigurationsExporter).inSingletonScope(); container.bind(ExportConfigurationsManager).toSelf().inSingletonScope(); diff --git a/plugins/task-plugin/src/export/export-configs-manager.ts b/plugins/task-plugin/src/export/export-configs-manager.ts index ea623fd21..9021f6f41 100644 --- a/plugins/task-plugin/src/export/export-configs-manager.ts +++ b/plugins/task-plugin/src/export/export-configs-manager.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -11,6 +11,7 @@ import { inject, injectable, multiInject } from 'inversify'; import { CheWorkspaceClient } from '../che-workspace-client'; +import { LaunchConfigurationsExporter } from './launch-configs-exporter'; import { che as cheApi } from '@eclipse-che/api'; export const ConfigurationsExporter = Symbol('ConfigurationsExporter'); @@ -42,11 +43,21 @@ export class ExportConfigurationsManager { @multiInject(ConfigurationsExporter) protected readonly exporters: ConfigurationsExporter[]; + @inject(LaunchConfigurationsExporter) + protected readonly launchConfigurationsExporter: LaunchConfigurationsExporter; + + protected cheCommands: cheApi.workspace.Command[] = []; + + async init(): Promise { + this.cheCommands = await this.cheWorkspaceClient.getCommands(); + this.launchConfigurationsExporter.init(this.cheCommands); + this.export(); + } + async export(): Promise { const exportPromises = []; - const cheCommands = await this.cheWorkspaceClient.getCommands(); for (const exporter of this.exporters) { - exportPromises.push(this.doExport(cheCommands, exporter)); + exportPromises.push(this.doExport(this.cheCommands, exporter)); } await Promise.all(exportPromises); diff --git a/plugins/task-plugin/src/export/launch-configs-exporter.ts b/plugins/task-plugin/src/export/launch-configs-exporter.ts index 39b407ea3..70b5abfcb 100644 --- a/plugins/task-plugin/src/export/launch-configs-exporter.ts +++ b/plugins/task-plugin/src/export/launch-configs-exporter.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,10 +8,11 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ +import * as startPoint from '../task-plugin-backend'; import * as theia from '@theia/plugin'; +import { ensureDirExists, modify, writeFile } from '../utils'; import { inject, injectable } from 'inversify'; -import { modify, writeFileSync } from '../utils'; import { ConfigFileLaunchConfigsExtractor } from '../extract/config-file-launch-configs-extractor'; import { ConfigurationsExporter } from './export-configs-manager'; @@ -32,37 +33,51 @@ export class LaunchConfigurationsExporter implements ConfigurationsExporter { @inject(VsCodeLaunchConfigsExtractor) protected readonly vsCodeLaunchConfigsExtractor: VsCodeLaunchConfigsExtractor; - async export(commands: cheApi.workspace.Command[]): Promise { - if (!theia.workspace.workspaceFolders) { + async init(commands: cheApi.workspace.Command[]): Promise { + theia.workspace.onDidChangeWorkspaceFolders( + event => { + const workspaceFolders: theia.WorkspaceFolder[] | undefined = event.added; + if (workspaceFolders && workspaceFolders.length > 0) { + this.export(commands, workspaceFolders); + } + }, + undefined, + startPoint.getSubscriptions() + ); + } + + async export(commands: cheApi.workspace.Command[], workspaceFolders?: theia.WorkspaceFolder[]): Promise { + workspaceFolders = workspaceFolders ? workspaceFolders : theia.workspace.workspaceFolders; + if (!workspaceFolders) { return; } const exportConfigsPromises: Promise[] = []; - for (const workspaceFolder of theia.workspace.workspaceFolders) { + for (const workspaceFolder of workspaceFolders) { exportConfigsPromises.push(this.doExport(workspaceFolder, commands)); } await Promise.all(exportConfigsPromises); } async doExport(workspaceFolder: theia.WorkspaceFolder, commands: cheApi.workspace.Command[]): Promise { - const launchConfigFileUri = this.getConfigFileUri(workspaceFolder.uri.path); - const configFileConfigs = this.configFileLaunchConfigsExtractor.extract(launchConfigFileUri); + const workspaceFolderPath = workspaceFolder.uri.path; + const launchConfigFilePath = resolve(workspaceFolderPath, CONFIG_DIR, LAUNCH_CONFIG_FILE); + const configFileConfigs = await this.configFileLaunchConfigsExtractor.extract(launchConfigFilePath); const vsCodeConfigs = this.vsCodeLaunchConfigsExtractor.extract(commands); const configFileContent = configFileConfigs.content; if (configFileContent) { - this.saveConfigs( - launchConfigFileUri, + return this.saveConfigs( + workspaceFolderPath, configFileContent, this.merge(configFileConfigs.configs, vsCodeConfigs.configs, this.getConsoleConflictLogger()) ); - return; } const vsCodeConfigsContent = vsCodeConfigs.content; if (vsCodeConfigsContent) { - this.saveConfigs(launchConfigFileUri, vsCodeConfigsContent, vsCodeConfigs.configs); + return this.saveConfigs(workspaceFolderPath, vsCodeConfigsContent, vsCodeConfigs.configs); } } @@ -100,13 +115,32 @@ export class LaunchConfigurationsExporter implements ConfigurationsExporter { return JSON.stringify(properties1) === JSON.stringify(properties2); } - private getConfigFileUri(rootDir: string): string { - return resolve(rootDir.toString(), CONFIG_DIR, LAUNCH_CONFIG_FILE); - } + private async saveConfigs( + workspaceFolderPath: string, + content: string, + configurations: theia.DebugConfiguration[] + ): Promise { + /* + There is an issue related to file watchers: the watcher only reports the first directory when creating recursively directories. + For example: + - we would like to create /projects/someProject/.theia/launch.json recursively + - /projects/someProject directory already exists + - .theia directory and launch.json file should be created + - as result file watcher fires an event that .theia directory was created, there is no an event about launch.json file + + The issue is reproduced not permanently. + + We had to use the workaround to avoid the issue: first we create the directory and then - config file + */ + + const configDirPath = resolve(workspaceFolderPath, CONFIG_DIR); + await ensureDirExists(configDirPath); + + const launchConfigFilePath = resolve(configDirPath, LAUNCH_CONFIG_FILE); + await ensureDirExists(launchConfigFilePath); - private saveConfigs(launchConfigFileUri: string, content: string, configurations: theia.DebugConfiguration[]): void { const result = modify(content, ['configurations'], configurations, formattingOptions); - writeFileSync(launchConfigFileUri, result); + return writeFile(launchConfigFilePath, result); } private getConsoleConflictLogger(): (config1: theia.DebugConfiguration, config2: theia.DebugConfiguration) => void { diff --git a/plugins/task-plugin/src/export/task-configs-exporter.ts b/plugins/task-plugin/src/export/task-configs-exporter.ts index 370dcb7ad..104820212 100644 --- a/plugins/task-plugin/src/export/task-configs-exporter.ts +++ b/plugins/task-plugin/src/export/task-configs-exporter.ts @@ -11,7 +11,7 @@ import * as startPoint from '../task-plugin-backend'; import { inject, injectable } from 'inversify'; -import { modify, writeFileSync } from '../utils'; +import { modify, writeFile } from '../utils'; import { BackwardCompatibilityResolver } from '../task/backward-compatibility'; import { CheTaskConfigsExtractor } from '../extract/che-task-configs-extractor'; @@ -49,7 +49,7 @@ export class TaskConfigurationsExporter implements ConfigurationsExporter { protected readonly backwardCompatibilityResolver: BackwardCompatibilityResolver; async export(commands: cheApi.workspace.Command[]): Promise { - const configFileTasks = this.configFileTasksExtractor.extract(THEIA_USER_TASKS_PATH); + const configFileTasks = await this.configFileTasksExtractor.extract(THEIA_USER_TASKS_PATH); const cheTasks = this.cheTaskConfigsExtractor.extract(commands); const vsCodeTasks = this.vsCodeTaskConfigsExtractor.extract(commands); @@ -58,22 +58,20 @@ export class TaskConfigurationsExporter implements ConfigurationsExporter { const configFileContent = configFileTasks.content; if (configFileContent) { - this.saveConfigs( + return this.saveConfigs( THEIA_USER_TASKS_PATH, configFileContent, this.merge(configFileConfigs, devfileConfigs, this.getConsoleConflictLogger()) ); - return; } const vsCodeTasksContent = vsCodeTasks.content; if (vsCodeTasksContent) { - this.saveConfigs(THEIA_USER_TASKS_PATH, vsCodeTasksContent, devfileConfigs); - return; + return this.saveConfigs(THEIA_USER_TASKS_PATH, vsCodeTasksContent, devfileConfigs); } if (cheTasks) { - this.saveConfigs(THEIA_USER_TASKS_PATH, '', cheTasks); + return this.saveConfigs(THEIA_USER_TASKS_PATH, '', cheTasks); } } @@ -112,14 +110,18 @@ export class TaskConfigurationsExporter implements ConfigurationsExporter { return JSON.stringify(properties1) === JSON.stringify(properties2); } - private saveConfigs(tasksConfigFileUri: string, content: string, configurations: TaskConfiguration[]): void { + private async saveConfigs( + tasksConfigFileUri: string, + content: string, + configurations: TaskConfiguration[] + ): Promise { const result = modify( content, ['tasks'], configurations.map(config => Object.assign(config, { _scope: undefined })), formattingOptions ); - writeFileSync(tasksConfigFileUri, result); + return writeFile(tasksConfigFileUri, result); } private getOutputChannelConflictLogger(): (config1: TaskConfiguration, config2: TaskConfiguration) => void { diff --git a/plugins/task-plugin/src/extract/config-file-launch-configs-extractor.ts b/plugins/task-plugin/src/extract/config-file-launch-configs-extractor.ts index 6aa8bff3d..83631182c 100644 --- a/plugins/task-plugin/src/extract/config-file-launch-configs-extractor.ts +++ b/plugins/task-plugin/src/extract/config-file-launch-configs-extractor.ts @@ -10,7 +10,7 @@ import * as theia from '@theia/plugin'; -import { parse, readFileSync } from '../utils'; +import { parse, readFile } from '../utils'; import { Configurations } from '../export/export-configs-manager'; import { injectable } from 'inversify'; @@ -18,8 +18,8 @@ import { injectable } from 'inversify'; /** Extracts launch configurations from config file by given uri. */ @injectable() export class ConfigFileLaunchConfigsExtractor { - extract(launchConfigFileUri: string): Configurations { - const configsContent = readFileSync(launchConfigFileUri); + async extract(launchConfigFileUri: string): Promise> { + const configsContent = await readFile(launchConfigFileUri); const configsJson = parse(configsContent); if (!configsJson || !configsJson.configurations) { return { content: '', configs: [] }; diff --git a/plugins/task-plugin/src/extract/config-file-task-configs-extractor.ts b/plugins/task-plugin/src/extract/config-file-task-configs-extractor.ts index 21bf2419c..5ce6d4d87 100644 --- a/plugins/task-plugin/src/extract/config-file-task-configs-extractor.ts +++ b/plugins/task-plugin/src/extract/config-file-task-configs-extractor.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ -import { parse, readFileSync } from '../utils'; +import { parse, readFile } from '../utils'; import { Configurations } from '../export/export-configs-manager'; import { TaskConfiguration } from '@eclipse-che/plugin'; @@ -17,8 +17,8 @@ import { injectable } from 'inversify'; /** Extracts configurations of tasks from config file by given uri. */ @injectable() export class ConfigFileTasksExtractor { - extract(tasksConfigFileUri: string): Configurations { - const tasksContent = readFileSync(tasksConfigFileUri); + async extract(tasksConfigFileUri: string): Promise> { + const tasksContent = await readFile(tasksConfigFileUri); const tasksJson = parse(tasksContent); if (!tasksJson || !tasksJson.tasks) { return { content: '', configs: [] }; diff --git a/plugins/task-plugin/src/task-plugin-backend.ts b/plugins/task-plugin/src/task-plugin-backend.ts index 31f2efb49..70c914f5f 100644 --- a/plugins/task-plugin/src/task-plugin-backend.ts +++ b/plugins/task-plugin/src/task-plugin-backend.ts @@ -53,7 +53,7 @@ export async function start(context: theia.PluginContext): Promise { await che.task.addTaskSubschema(CHE_TASK_SCHEMA); const exportConfigurationsManager = container.get(ExportConfigurationsManager); - exportConfigurationsManager.export(); + exportConfigurationsManager.init(); const taskStatusHandler = container.get(TaskStatusHandler); taskStatusHandler.init(); diff --git a/plugins/task-plugin/src/utils.ts b/plugins/task-plugin/src/utils.ts index 40097e0ca..5ad9f4b4d 100644 --- a/plugins/task-plugin/src/utils.ts +++ b/plugins/task-plugin/src/utils.ts @@ -8,6 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ +import * as fs from 'fs-extra'; import * as jsoncparser from 'jsonc-parser'; import * as path from 'path'; @@ -16,8 +17,6 @@ import { FormattingOptions, JSONPath, ParseError } from 'jsonc-parser'; import { URL } from 'url'; import { resolve } from 'path'; -const fs = require('fs'); - /** Allows to get attribute by given name, returns `undefined` if attribute is not found */ export function getAttribute(attributeName: string, attributes?: { [key: string]: string }): string | undefined { if (!attributes) { @@ -102,12 +101,31 @@ export function readFileSync(filePath: string): string { } } +/** Asynchronously reads the file by given path. Returns content of the file or empty string if file doesn't exist */ +export async function readFile(filePath: string): Promise { + try { + if (await fs.pathExists(filePath)) { + return fs.readFile(filePath, 'utf8'); + } + return ''; + } catch (e) { + console.error(e); + return ''; + } +} + /** Synchronously writes given content to the file. Creates directories to the file if they don't exist */ export function writeFileSync(filePath: string, content: string): void { ensureDirExistence(filePath); fs.writeFileSync(filePath, content); } +/** Asynchronously writes given content to the file. Creates directories to the file if they don't exist */ +export async function writeFile(filePath: string, content: string): Promise { + await ensureDirExists(filePath); + return fs.writeFile(filePath, content); +} + /** Synchronously creates a directory to the file if they don't exist */ export function ensureDirExistence(filePath: string): void { const dirName = path.dirname(filePath); @@ -116,3 +134,12 @@ export function ensureDirExistence(filePath: string): void { } fs.mkdirSync(dirName, { recursive: true }); } + +/** Creates a directory containing the file if they don't exist */ +export async function ensureDirExists(filePath: string): Promise { + const dirName = path.dirname(filePath); + if (await fs.pathExists(dirName)) { + return; + } + return fs.mkdirp(dirName); +} diff --git a/plugins/welcome-plugin/src/welcome-plugin.ts b/plugins/welcome-plugin/src/welcome-plugin.ts index 7dd80a760..f937babd8 100644 --- a/plugins/welcome-plugin/src/welcome-plugin.ts +++ b/plugins/welcome-plugin/src/welcome-plugin.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2020 Red Hat, Inc. + * Copyright (c) 2020-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -53,23 +53,33 @@ async function getHtmlForWebview(context: theia.PluginContext): Promise } // Open Readme file is there is one -export async function handleReadmeFiles(): Promise { - const roots: theia.WorkspaceFolder[] | undefined = theia.workspace.workspaceFolders; - // In case of only one workspace - if (roots && roots.length === 1) { - const children = await theia.workspace.findFiles('README.md', 'node_modules/**', 1); - const updatedChildren = children.filter((child: theia.Uri) => { - if (child.fsPath.indexOf('node_modules') === -1) { - return child; - } - }); +export async function handleReadmeFiles( + readmeHandledCallback?: () => void, + roots?: theia.WorkspaceFolder[] +): Promise { + roots = roots ? roots : theia.workspace.workspaceFolders; + if (!roots || roots.length < 1) { + return; + } - if (updatedChildren.length >= 1) { - const openPath = theia.Uri.parse(updatedChildren[0] + '?open-handler=code-editor-preview'); - const doc: theia.TextDocument | undefined = await theia.workspace.openTextDocument(openPath); - if (doc) { - theia.window.showTextDocument(doc); - } + const children = await theia.workspace.findFiles('README.md', 'node_modules/**', 1); + const updatedChildren = children.filter((child: theia.Uri) => { + if (child.fsPath.indexOf('node_modules') === -1) { + return child; + } + }); + + if (updatedChildren.length < 1) { + return; + } + + const openPath = theia.Uri.parse(updatedChildren[0] + '?open-handler=code-editor-preview'); + const doc: theia.TextDocument | undefined = await theia.workspace.openTextDocument(openPath); + if (doc) { + theia.window.showTextDocument(doc); + + if (readmeHandledCallback) { + readmeHandledCallback(); } } } @@ -122,18 +132,42 @@ export function start(context: theia.PluginContext): void { showWelcomePage = configuration.get(Settings.SHOW_WELCOME_PAGE); } - if (showWelcomePage && theia.window.visibleTextEditors.length === 0) { - setTimeout(async () => { - addPanel(context); - - const workspacePlugin = theia.plugins.getPlugin('Eclipse Che.@eclipse-che/workspace-plugin'); - if (workspacePlugin) { - workspacePlugin.exports.onDidCloneSources(() => handleReadmeFiles()); - } else { - handleReadmeFiles(); - } - }, 100); + if (!showWelcomePage || theia.window.visibleTextEditors.length > 0) { + return; } + + let cloneSourcesDisposable: theia.Disposable | undefined = undefined; + setTimeout(async () => { + addPanel(context); + + const workspacePlugin = theia.plugins.getPlugin('Eclipse Che.@eclipse-che/workspace-plugin'); + if (workspacePlugin && workspacePlugin.exports) { + // it handles the case when the multi-root mode is OFF + // we should remove this logic when we switch to the multi-root mode is ON by default + cloneSourcesDisposable = workspacePlugin.exports.onDidCloneSources( + () => handleReadmeFiles(readmeHandledCallback), + undefined, + context.subscriptions + ); + } else { + handleReadmeFiles(); + } + }, 100); + + // handles the case when the multi-root mode is ON + const changeWorkspaceFoldersDisposable = theia.workspace.onDidChangeWorkspaceFolders( + event => handleReadmeFiles(readmeHandledCallback, event.added), + undefined, + context.subscriptions + ); + + const readmeHandledCallback = () => { + changeWorkspaceFoldersDisposable.dispose(); + + if (cloneSourcesDisposable) { + cloneSourcesDisposable.dispose(); + } + }; } export function stop(): void {} diff --git a/plugins/workspace-plugin/src/theia-commands.ts b/plugins/workspace-plugin/src/theia-commands.ts index 20d40daa7..1f2ff5d17 100644 --- a/plugins/workspace-plugin/src/theia-commands.ts +++ b/plugins/workspace-plugin/src/theia-commands.ts @@ -44,7 +44,8 @@ function isDevfileProjectConfig( } export interface TheiaImportCommand { - execute(): Promise; + /** @returns the path to the imported project */ + execute(): Promise; } export function buildProjectImportCommand( @@ -119,7 +120,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { this.projectsRoot = projectsRoot; } - clone(): PromiseLike { + clone(): PromiseLike { return theia.window.withProgress( { location: theia.ProgressLocation.Notification, @@ -135,7 +136,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { ); } - async execute(): Promise { + async execute(): Promise { if (!git.isSecureGitURI(this.locationURI)) { // clone using regular URI return this.clone(); @@ -203,7 +204,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { continue; } // skip - return; + return Promise.reject(new Error(message)); } // pause will be removed after debugging this method @@ -223,7 +224,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { private async gitClone( progress: theia.Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken - ): Promise { + ): Promise { const args: string[] = ['clone', this.locationURI, this.projectPath]; if (this.checkoutBranch) { args.push('--branch'); @@ -260,9 +261,11 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { } else { theia.window.showInformationMessage(`${messageStart}.`); } + return this.projectPath; } catch (e) { theia.window.showErrorMessage(`Couldn't clone ${this.locationURI}: ${e.message}`); console.log(`Couldn't clone ${this.locationURI}`, e); + throw new Error(e); } } @@ -270,7 +273,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { private async gitSparseCheckout( progress: theia.Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken - ): Promise { + ): Promise { if (!this.sparseCheckoutDir) { throw new Error('Parameter "sparseCheckoutDir" is not set for "' + this.projectName + '" project.'); } @@ -291,6 +294,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { theia.window.showInformationMessage( `Sources by template ${this.sparseCheckoutDir} of ${this.locationURI} was cloned to ${this.projectPath}.` ); + return this.projectPath; } } @@ -316,11 +320,11 @@ export class TheiaImportZipCommand implements TheiaImportCommand { } } - async execute(): Promise { + async execute(): Promise { const importZip = async ( progress: theia.Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken - ): Promise => { + ): Promise => { try { // download const curlArgs = ['-sSL', '--output', this.zipfilePath]; @@ -345,9 +349,11 @@ export class TheiaImportZipCommand implements TheiaImportCommand { if (zipfileParentDir.indexOf(os.tmpdir() + path.sep) === 0) { fs.rmdirSync(zipfileParentDir); } + return this.projectDir; } catch (e) { theia.window.showErrorMessage(`Couldn't import ${this.locationURI}: ${e.message}`); console.error(`Couldn't import ${this.locationURI}`, e); + throw new Error(e); } }; diff --git a/plugins/workspace-plugin/src/workspace-folder-updater.ts b/plugins/workspace-plugin/src/workspace-folder-updater.ts new file mode 100644 index 000000000..f61220b3a --- /dev/null +++ b/plugins/workspace-plugin/src/workspace-folder-updater.ts @@ -0,0 +1,85 @@ +/********************************************************************** + * 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 theia from '@theia/plugin'; +const UPDATE_WORKSPACE_FOLDER_TIMEOUT = 5000; + +export class WorkspaceFolderUpdater { + private pendingFolders: string[] = []; + private addingWorkspaceFolderPromise: Promise | undefined; + + async addWorkspaceFolder(path: string): Promise { + const workspaceFolderPath = this.toValidWorkspaceFolderPath(path); + if (this.pendingFolders.includes(workspaceFolderPath)) { + return Promise.resolve(); + } + + if (this.addingWorkspaceFolderPromise) { + this.pendingFolders.push(workspaceFolderPath); + } else { + try { + this.addingWorkspaceFolderPromise = this.addFolder(workspaceFolderPath); + await this.addingWorkspaceFolderPromise; + } catch (error) { + console.error(error); + } finally { + this.addingWorkspaceFolderPromise = undefined; + } + + const next = this.pendingFolders.shift(); + if (next) { + this.addWorkspaceFolder(next); + } + } + return Promise.resolve(); + } + + protected addFolder(projectPath: string): Promise { + const isProjectFolder = (folder: theia.WorkspaceFolder) => folder.uri.path === projectPath; + const workspaceFolders = theia.workspace.workspaceFolders || []; + if (workspaceFolders.some(isProjectFolder)) { + return Promise.resolve(undefined); + } + + return new Promise((resolve, reject) => { + const disposable = theia.workspace.onDidChangeWorkspaceFolders(event => { + const existingWorkspaceFolders = theia.workspace.workspaceFolders || []; + if (event.added.some(isProjectFolder) || existingWorkspaceFolders.some(isProjectFolder)) { + clearTimeout(addFolderTimeout); + + disposable.dispose(); + + resolve(); + } + }); + + const addFolderTimeout = setTimeout(() => { + disposable.dispose(); + + reject( + new Error( + `Adding workspace folder ${projectPath} was cancelled by timeout ${UPDATE_WORKSPACE_FOLDER_TIMEOUT} ms` + ) + ); + }, UPDATE_WORKSPACE_FOLDER_TIMEOUT); + + theia.workspace.updateWorkspaceFolders(workspaceFolders ? workspaceFolders.length : 0, undefined, { + uri: theia.Uri.file(projectPath), + }); + }); + } + + protected toValidWorkspaceFolderPath(path: string): string { + if (path.endsWith('/')) { + return path.slice(0, -1); + } + return path; + } +} diff --git a/plugins/workspace-plugin/src/workspace-projects-manager.ts b/plugins/workspace-plugin/src/workspace-projects-manager.ts index 1f8d3322d..3c345ee07 100644 --- a/plugins/workspace-plugin/src/workspace-projects-manager.ts +++ b/plugins/workspace-plugin/src/workspace-projects-manager.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2018-2020 Red Hat, Inc. + * Copyright (c) 2018-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -18,6 +18,7 @@ import * as theia from '@theia/plugin'; import { TheiaImportCommand, buildProjectImportCommand } from './theia-commands'; +import { WorkspaceFolderUpdater } from './workspace-folder-updater'; import { che as cheApi } from '@eclipse-che/api'; const onDidCloneSourcesEmitter = new theia.EventEmitter(); @@ -30,19 +31,29 @@ export function handleWorkspaceProjects(pluginContext: theia.PluginContext, proj } export class WorkspaceProjectsManager { - watchers: theia.FileSystemWatcher[] = []; + protected watchers: theia.FileSystemWatcher[] = []; + protected workspaceFolderUpdater = new WorkspaceFolderUpdater(); constructor(protected pluginContext: theia.PluginContext, protected projectsRoot: string) {} - async run(): Promise { - if (!theia.workspace.name) { - // no workspace opened, so nothing to clone / watch - return; - } + getProjectPath(project: cheApi.workspace.devfile.Project): string { + return project.clonePath + ? path.join(this.projectsRoot, project.clonePath) + : path.join(this.projectsRoot, project.name!); + } + + getProjects(workspace: cheApi.workspace.Workspace): cheApi.workspace.devfile.Project[] { + const projects = workspace.devfile!.projects; + return projects ? projects : []; + } + async run(): Promise { const workspace = await che.workspace.getCurrentWorkspace(); const cloneCommandList = await this.buildCloneCommands(workspace); - await this.executeCloneCommands(cloneCommandList); + + const cloningPromise = this.executeCloneCommands(cloneCommandList, workspace); + theia.window.withProgress({ location: { viewId: 'explorer' } }, () => cloningPromise); + await cloningPromise; await this.startSyncWorkspaceProjects(); } @@ -50,28 +61,41 @@ export class WorkspaceProjectsManager { async buildCloneCommands(workspace: cheApi.workspace.Workspace): Promise { const instance = this; - const projects = workspace.devfile!.projects; - if (!projects) { - return []; - } + const projects = this.getProjects(workspace); return projects - .filter(project => { - const projectPath = project.clonePath - ? path.join(instance.projectsRoot, project.clonePath) - : path.join(instance.projectsRoot, project.name!); - return !fs.existsSync(projectPath); - }) + .filter(project => !fs.existsSync(this.getProjectPath(project))) .map(project => buildProjectImportCommand(project, instance.projectsRoot)!); } - private async executeCloneCommands(cloneCommandList: TheiaImportCommand[]): Promise { + private async executeCloneCommands( + cloneCommandList: TheiaImportCommand[], + workspace: cheApi.workspace.Workspace + ): Promise { if (cloneCommandList.length === 0) { return; } theia.window.showInformationMessage('Che Workspace: Starting importing projects.'); - await Promise.all(cloneCommandList.map(cloneCommand => cloneCommand.execute())); + + const isMultiRoot = isMultiRootWorkspace(workspace); + + const cloningPromises: PromiseLike[] = []; + for (const cloneCommand of cloneCommandList) { + try { + const cloningPromise = cloneCommand.execute(); + cloningPromises.push(cloningPromise); + + if (isMultiRoot) { + cloningPromise.then(projectPath => this.workspaceFolderUpdater.addWorkspaceFolder(projectPath)); + } + } catch (e) { + // we continue to clone other projects even if a clone process failed for a project + } + } + + await Promise.all(cloningPromises); + theia.window.showInformationMessage('Che Workspace: Finished importing projects.'); onDidCloneSourcesEmitter.fire(); } @@ -144,3 +168,8 @@ export class WorkspaceProjectsManager { ); } } + +function isMultiRootWorkspace(workspace: cheApi.workspace.Workspace): boolean { + const devfile = workspace.devfile; + return !!devfile && !!devfile.attributes && !!devfile.attributes.multiRoot && devfile.attributes.multiRoot === 'on'; +}