Skip to content

Commit

Permalink
multi-root workspace support, vsCode compatibility
Browse files Browse the repository at this point in the history
The patch 80f402c added the support of having multiple roots in the same workspace. In that patch, workspace meta data is stored in the `.theia` folder of the first root, and brings up the following issues / difficulties:
- using the same first root in more than one workspace becomes impossible.
- no flexibility of naming the workspace or deciding where to store the workspace meta data.
- theia opens the implicit workspace folder after all roots are removed from the workspace, causing confusion.
- preferences of the workspace folder is used across the all roots in the same workspace.
- workspace meta data is stored in more than one file, and the format is incompatible with most other (if not all) IDEs.

This is the 2nd patch for eclipse-theia#1660.

What is included in this PR:
- workspace file becomes independent from the root folders. It is users' choice to decide where to store the information, and what to name the workspace.
- theia prompts users to save the workspace data in a user-specified place, if the workspace does not have a user-specified place to store the meta data.
- workspaces can be recreated from the workspace file
- "Save workspace As..." is available for users to rename workspace or change location of the workspace file.
- the format of theia workspace file is compatible with that of vsCode. Paths relative to the parent folder of the workspace file are used where applicable
- a multi root workspace can be opened by theia by running `yarn start <path of the workpsace file>`

What is not inlcuded in this PR:
- the workspace preferences should be saved as part of the meta data file.
- root display names are not customizable by adding the folders -> name property in the workspace file.

Signed-off-by: elaihau <liang.huang@ericsson.com>
  • Loading branch information
elaihau authored and elaihau committed Sep 20, 2018
1 parent 971b421 commit 543b119
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 208 deletions.
11 changes: 10 additions & 1 deletion packages/filesystem/src/common/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export interface FileSystem extends JsonRpcServer<FileSystemClient> {
}

export interface FileMoveOptions {
overwrite?: boolean;
overwrite?: boolean;
}

export interface FileDeleteOptions {
Expand Down Expand Up @@ -176,6 +176,15 @@ export namespace FileStat {
&& candidate.hasOwnProperty('lastModification')
&& candidate.hasOwnProperty('isDirectory');
}

export function equals(one: object | undefined, other: object | undefined): boolean {
if (!one || !other || !is(one) || !is(other)) {
return false;
}
return one.uri === other.uri
&& one.lastModification === other.lastModification
&& one.isDirectory === other.isDirectory;
}
}

export namespace FileSystemError {
Expand Down
11 changes: 9 additions & 2 deletions packages/git/src/browser/git-repository-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Git, Repository } from '../common';
import { injectable, inject } from 'inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { Event, Emitter } from '@theia/core';

export interface GitRefreshOptions {
Expand All @@ -32,7 +33,8 @@ export class GitRepositoryProvider {

constructor(
@inject(Git) protected readonly git: Git,
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService,
@inject(FileSystem) protected readonly fileSystem: FileSystem
) {
this.initialize();
}
Expand Down Expand Up @@ -77,7 +79,12 @@ export class GitRepositoryProvider {
}

async refresh(options?: GitRefreshOptions): Promise<void> {
const roots = await this.workspaceService.roots;
const roots: FileStat[] = [];
for (const root of await this.workspaceService.roots) {
if (await this.fileSystem.exists(root.uri)) {
roots.push(root);
}
}
const repoUris = new Map<string, Repository>();
const reposOfRoots = await Promise.all(
roots.map(r => this.git.repositories(r.uri, { ...options }))
Expand Down
3 changes: 1 addition & 2 deletions packages/git/src/browser/git-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import URI from '@theia/core/lib/common/uri';
import { ResourceProvider, CommandService, MenuPath } from '@theia/core';
import { ContextMenuRenderer, LabelProvider, DiffUris, StatefulWidget, Message } from '@theia/core/lib/browser';
import { EditorManager, EditorWidget, EditorOpenerOptions } from '@theia/editor/lib/browser';
import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { Git, GitFileChange, GitFileStatus, Repository, WorkingDirectoryStatus, CommitWithChanges } from '../common';
import { GitWatcher, GitStatusChangeEvent } from '../common/git-watcher';
import { GIT_RESOURCE_SCHEME } from './git-resource';
Expand Down Expand Up @@ -76,7 +76,6 @@ export class GitWidget extends ReactWidget implements StatefulWidget {
@inject(CommandService) protected readonly commandService: CommandService,
@inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider,
@inject(LabelProvider) protected readonly labelProvider: LabelProvider,
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService,
@inject(GitAvatarService) protected readonly avatarService: GitAvatarService,
@inject(GitCommitMessageValidator) protected readonly commitMessageValidator: GitCommitMessageValidator) {

Expand Down
4 changes: 2 additions & 2 deletions packages/navigator/src/browser/navigator-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export class FileNavigatorModel extends FileTreeModel {
}

protected async createRoot(): Promise<TreeNode | undefined> {
const roots = await this.workspaceService.roots;
if (roots.length > 0) {
if (this.workspaceService.opened) {
const workspaceNode = WorkspaceNode.createRoot();
const roots = await this.workspaceService.roots;
for (const root of roots) {
workspaceNode.children.push(
await this.tree.createWorkspaceRoot(root, workspaceNode)
Expand Down
5 changes: 3 additions & 2 deletions packages/navigator/src/browser/navigator-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import URI from '@theia/core/lib/common/uri';
import { SelectionService, CommandService } from '@theia/core/lib/common';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { CommandService, SelectionService } from '@theia/core/lib/common';
import { CommonCommands } from '@theia/core/lib/browser';
import {
ContextMenuRenderer, ExpandableTreeNode,
TreeProps, TreeModel, TreeNode,
Expand Down Expand Up @@ -187,4 +187,5 @@ export class FileNavigatorWidget extends FileTreeWidget {
</div>
</div>;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
KeyModifier, KeybindingRegistry
} from '@theia/core/lib/browser';
import { WidgetManager } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions } from './terminal-widget-impl';
import { TerminalKeybindingContexts } from './terminal-keybinding-contexts';
import { TerminalService } from './base/terminal-service';
Expand All @@ -50,8 +49,7 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon

constructor(
@inject(ApplicationShell) protected readonly shell: ApplicationShell,
@inject(WidgetManager) protected readonly widgetManager: WidgetManager,
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService
@inject(WidgetManager) protected readonly widgetManager: WidgetManager
) { }

registerCommands(commands: CommandRegistry): void {
Expand Down
44 changes: 24 additions & 20 deletions packages/workspace/src/browser/quick-open-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
********************************************************************************/

import { injectable, inject } from 'inversify';
import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenGroupItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/';
import { WorkspaceService } from './workspace-service';
import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenGroupItem, QuickOpenMode, LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService, getTemporaryWorkspaceFileUri } from './workspace-service';
import { WorkspacePreferences } from './workspace-preferences';
import URI from '@theia/core/lib/common/uri';
import { MessageService } from '@theia/core/lib/common';
import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common';
Expand All @@ -31,42 +32,45 @@ export class QuickOpenWorkspace implements QuickOpenModel {
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(FileSystem) protected readonly fileSystem: FileSystem;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(WorkspacePreferences) protected preferences: WorkspacePreferences;

async open(workspaces: string[]): Promise<void> {
this.items = [];
const homeStat = await this.fileSystem.getCurrentUserHome();
const home = (homeStat) ? new URI(homeStat.uri).withoutScheme().toString() : undefined;

let tempWorkspaceFile: URI | undefined;
if (home) {
tempWorkspaceFile = getTemporaryWorkspaceFileUri(new URI(home));
}
await this.preferences.ready;
for (const workspace of workspaces) {
const uri = new URI(workspace);
const stat = await this.fileSystem.getFileStat(workspace);
if (!stat) {
continue;
if (!stat ||
!this.preferences['workspace.supportMultiRootWorkspace'] && !stat.isDirectory) {
continue; // skip the workspace files if multi root is not supported
}
if (tempWorkspaceFile && uri.toString() === tempWorkspaceFile.toString()) {
continue; // skip the temporary workspace files
}
const lastModification = moment(stat.lastModification).fromNow();
this.items.push(new QuickOpenGroupItem({
label: uri.path.base,
description: (home) ? FileSystemUtils.tildifyPath(uri.path.toString(), home) : uri.path.toString(),
groupLabel: (workspace === workspaces[0]) ? 'Current Workspace' : `Modified ${lastModification}`,
iconClass: await this.labelProvider.getIcon(uri) + ' file-icon',
run: (mode: QuickOpenMode): boolean => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
this.workspaceService.roots.then(roots => {
const current = roots[0];
if (current === undefined) { // Available recent workspace(s) but closed
if (workspace && workspace.length > 0) {
this.workspaceService.open(new URI(workspace));
}
} else {
if (current.uri !== workspace) {
this.workspaceService.open(new URI(workspace));
} else {
this.messageService.info(`Using the same workspace [ ${name} ]`);
}

}
});
const current = this.workspaceService.workspace;
const uriToOpen = new URI(workspace);
if (current && current.uri === workspace) {
this.messageService.info(`Using the same workspace [ ${uriToOpen.displayName} ]`);
} else {
this.workspaceService.open(uriToOpen);
}
return true;
},
}));
Expand Down
35 changes: 29 additions & 6 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export namespace WorkspaceCommands {
id: 'workspace:removeFolder',
label: 'Remove Folder from Workspace'
};
export const SAVE_WORKSPACE_AS: Command = {
id: 'workspace:saveAs',
label: 'Save Workspace As...'
};
}

@injectable()
Expand Down Expand Up @@ -230,14 +234,33 @@ export class WorkspaceCommandContribution implements CommandContribution {
isEnabled: () => this.workspaceService.isMultiRootWorkspaceOpened,
isVisible: uris => !uris.length || this.areWorkspaceRoots(uris),
execute: async uris => {
const node = await this.fileDialogService.showOpenDialog({ title: WorkspaceCommands.ADD_FOLDER.label! });
this.addFolderToWorkspace(node);
const node = await this.fileDialogService.showOpenDialog({
title: WorkspaceCommands.ADD_FOLDER.label!,
canSelectFiles: false,
canSelectFolders: true
});
if (!node) {
return;
}
const workspaceSavedBeforeAdding = this.workspaceService.saved;
await this.addFolderToWorkspace(node);
if (!workspaceSavedBeforeAdding) {
const saveCommand = registry.getCommand(WorkspaceCommands.SAVE_WORKSPACE_AS.id);
if (saveCommand && await new ConfirmDialog({
title: 'Folder added to Workspace',
msg: 'A workspace with multiple roots was created. Do you want to save your workspace configuration as a file?',
ok: 'Yes',
cancel: 'No'
}).open()) {
registry.executeCommand(saveCommand.id);
}
}
}
}));
registry.registerCommand(WorkspaceCommands.REMOVE_FOLDER, this.newMultiUriAwareCommandHandler({
execute: uris => this.removeFolderFromWorkspace(uris),
isEnabled: () => this.workspaceService.isMultiRootWorkspaceOpened,
isVisible: uris => this.areWorkspaceRoots(uris),
execute: uris => this.removeFolderFromWorkspace(uris)
isVisible: uris => this.areWorkspaceRoots(uris) && this.workspaceService.saved
}));
});
}
Expand Down Expand Up @@ -298,9 +321,9 @@ export class WorkspaceCommandContribution implements CommandContribution {
return parentUri.resolve(base);
}

protected addFolderToWorkspace(node: Readonly<FileStatNode> | undefined): void {
protected async addFolderToWorkspace(node: Readonly<FileStatNode> | undefined): Promise<void> {
if (node && node.fileStat.isDirectory) {
this.workspaceService.addRoot(node.uri);
await this.workspaceService.addRoot(node.uri);
}
}

Expand Down
Loading

0 comments on commit 543b119

Please sign in to comment.