Skip to content

Commit

Permalink
multi-root workspace support, minimal patch
Browse files Browse the repository at this point in the history
- minimal patch of #1660
- use the first root folder as the workspace folder. And the workspace config would be saved under that folder in the `root.json` file
- workspace folders can be added and removed as needed, while the first root folder cannot be removed from the workspace
- 'workspace.supportMultiRootWorkspace' is added as a preference, which users could toggle (true/false), to show & hide the "add/remove folder" menu items
- when add / remove folders to / from the workspace, use event to keep widgets up to date, instead of reloading the whole theia, whereas when users switch from one workspace to another in the same tab, or close the workspace, theia still reloads.
- extensions updated: workspace, git, & navigator

Signed-off-by: elaihau <liang.huang@ericsson.com>
  • Loading branch information
elaihau committed Jun 22, 2018
1 parent de618c7 commit a98a351
Show file tree
Hide file tree
Showing 35 changed files with 885 additions and 194 deletions.
10 changes: 8 additions & 2 deletions packages/core/src/browser/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ export abstract class AbstractDialog<T> extends BaseWidget {
@injectable()
export class ConfirmDialogProps extends DialogProps {
readonly msg: string | HTMLElement;
readonly hasCancel: boolean;
readonly cancel?: string;
readonly hasOk: boolean;
readonly ok?: string;
}

Expand All @@ -201,8 +203,12 @@ export class ConfirmDialog extends AbstractDialog<boolean> {
super(props);

this.contentNode.appendChild(this.createMessageNode(this.props.msg));
this.appendCloseButton(props.cancel);
this.appendAcceptButton(props.ok);
if (props.hasCancel) {
this.appendCloseButton(props.cancel);
}
if (props.hasOk) {
this.appendAcceptButton(props.ok);
}
}

protected onCloseRequest(msg: Message): void {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/browser/tree/tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
return this.tree.getNode(id);
}

getNodes(test: (node: TreeNode) => boolean): TreeNode[] {
return this.tree.getNodes(test);
}

validateNode(node: TreeNode | undefined) {
return this.tree.validateNode(node);
}
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/browser/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface Tree extends Disposable {
* Return a node for the given identifier or undefined if such does not exist.
*/
getNode(id: string | undefined): TreeNode | undefined;
/**
* Return an array of nodes that satisfy the given condition. An empty array is return when none is found.
*/
getNodes(test: (node: TreeNode) => boolean): TreeNode[];
/**
* Return a valid node in this tree matching to the given; otherwise undefined.
*/
Expand All @@ -41,7 +45,7 @@ export interface Tree extends Disposable {
*/
refresh(parent: Readonly<CompositeTreeNode>): Promise<void>;
/**
* Emit when the children of the give node are refreshed.
* Emit when the children of the given node are refreshed.
*/
readonly onNodeRefreshed: Event<Readonly<CompositeTreeNode>>;
}
Expand Down Expand Up @@ -204,6 +208,17 @@ export class TreeImpl implements Tree {
return id !== undefined ? this.nodes[id] : undefined;
}

getNodes(test: (node: TreeNode) => boolean): TreeNode[] {
const nodes: TreeNode[] = [];
Object.keys(this.nodes).forEach(nodeId => {
const node = this.nodes[nodeId];
if (node && test(node)) {
nodes.push(node);
}
});
return nodes;
}

validateNode(node: TreeNode | undefined): TreeNode | undefined {
const id = !!node ? node.id : undefined;
return this.getNode(id);
Expand Down
6 changes: 5 additions & 1 deletion packages/file-search/src/browser/quick-file-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export class QuickFileOpenService implements QuickOpenModel {
@inject(FileSearchService) protected readonly fileSearchService: FileSearchService,
@inject(LabelProvider) protected readonly labelProvider: LabelProvider
) {
workspaceService.root.then(root => this.wsRoot = root);
this.workspaceService.onWorkspaceRootChanged(event => {
if (event) {
this.wsRoot = event.root;
}
});
}

protected wsRoot: FileStat | undefined;
Expand Down
23 changes: 18 additions & 5 deletions packages/filesystem/src/browser/file-tree/file-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,16 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
if (this.isFileContentChanged(change)) {
return;
}
const parent = this.getNode(change.uri.parent.toString());
if (DirNode.is(parent) && parent.expanded) {
accept(parent);
const parentNodes = this.getNodes((node: FileStatNode) => change.uri.parent.toString() === node.uri.toString());
for (const parent of parentNodes) {
if (DirNode.is(parent) && parent.expanded) {
accept(parent);
}
}
}
protected isFileContentChanged(change: FileChange): boolean {
return change.type === FileChangeType.UPDATED && FileNode.is(this.getNode(change.uri.toString()));
const nodes = this.getNodes((node: FileStatNode) => change.uri.toString() === node.uri.toString());
return change.type === FileChangeType.UPDATED && nodes.length > 0 && FileNode.is(nodes[0]);
}

copy(uri: URI): boolean {
Expand All @@ -112,6 +115,10 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
* Move the given source file or directory to the given target directory.
*/
async move(source: TreeNode, target: TreeNode) {
if (source.parent && source.parent.name === 'WorkspaceRoot') {
// do not support moving the root folder
return;
}
if (DirNode.is(target) && FileStatNode.is(source)) {
const sourceUri = source.uri.toString();
if (target.uri.toString() === sourceUri) { /* Folder on itself */
Expand All @@ -124,6 +131,10 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
await this.fileSystem.move(sourceUri, targetUri, { overwrite: true });
// to workaround https://github.com/Axosoft/nsfw/issues/42
this.refresh(target);

if (source.parent) {
this.refresh(source.parent);
}
}
}
}
Expand All @@ -134,7 +145,9 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
title: 'Replace file',
msg: `File '${fileName}' already exists in the destination folder. Do you want to replace it?`,
ok: 'Yes',
cancel: 'No'
cancel: 'No',
hasOk: true,
hasCancel: true
});
return dialog.open();
}
Expand Down
28 changes: 18 additions & 10 deletions packages/filesystem/src/browser/file-tree/file-tree-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { injectable, inject } from "inversify";
import { h } from "@phosphor/virtualdom";
import { Message } from "@phosphor/messaging";
import {
ContextMenuRenderer,
TreeWidget, NodeProps, TreeProps, TreeNode
} from "@theia/core/lib/browser";
import { ElementAttrs } from "@phosphor/virtualdom";
import { DirNode, FileStatNode } from "./file-tree";
import { FileTreeModel } from "./file-tree-model";
import { injectable, inject } from 'inversify';
import { h } from '@phosphor/virtualdom';
import { Message } from '@phosphor/messaging';
import { ContextMenuRenderer, NodeProps, TreeDecoration, TreeNode, TreeProps, TreeWidget } from '@theia/core/lib/browser';
import { ElementAttrs } from '@phosphor/virtualdom';
import { DirNode, FileStatNode } from './file-tree';
import { FileTreeModel } from './file-tree-model';
import { DisposableCollection, Disposable } from '@theia/core/lib/common';

export const FILE_TREE_CLASS = 'theia-FileTree';
Expand Down Expand Up @@ -139,4 +136,15 @@ export class FileTreeWidget extends TreeWidget {
return this.model.getNode(id);
}

protected getDecorations(node: TreeNode): TreeDecoration.Data[] {
const treeNode = {
id: node.id,
name: node.name,
parent: node.parent
};
if (FileStatNode.is(node)) {
treeNode.id = FileStatNode.getUri(node);
}
return super.getDecorations(treeNode as TreeNode);
}
}
98 changes: 83 additions & 15 deletions packages/filesystem/src/browser/file-tree/file-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,51 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { injectable, inject } from "inversify";
import { injectable, inject } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { TreeNode, CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode, TreeImpl } from "@theia/core/lib/browser";
import { FileSystem, FileStat } from "../../common";
import { LabelProvider } from "@theia/core/lib/browser/label-provider";
import { TreeNode, CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode, TreeImpl } from '@theia/core/lib/browser';
import { FileSystem, FileStat } from '../../common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { UriSelection } from '@theia/core/lib/common//selection';

@injectable()
export class FileTree extends TreeImpl {

@inject(FileSystem) protected readonly fileSystem: FileSystem;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
hasVirtualRoot: boolean = false;

async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (FileStatNode.is(parent)) {
const fileStat = await this.resolveFileStat(parent);
let fileStat;
if (this.isVirtualRoot(parent)) {
fileStat = await this.resolveVirtualRootFileStat(parent);
if (fileStat) {
parent.fileStat = fileStat;
}
} else {
fileStat = await this.resolveFileStat(parent);
}

if (fileStat) {
return this.toNodes(fileStat, parent);
}

return [];
}
return super.resolveChildren(parent);
}

protected resolveFileStat(node: FileStatNode): Promise<FileStat | undefined> {
return this.fileSystem.getFileStat(node.fileStat.uri).then(fileStat => {
if (fileStat) {
node.fileStat = fileStat;
return fileStat;
}
return undefined;
});
protected resolveFileStat(node: CompositeTreeNode): Promise<FileStat | undefined> {
if (FileStatNode.is(node)) {
return this.fileSystem.getFileStat(node.fileStat.uri).then(fileStat => {
if (fileStat) {
node.fileStat = fileStat;
return fileStat;
}
return undefined;
});
}
return Promise.resolve(undefined);
}

protected async toNodes(fileStat: FileStat, parent: CompositeTreeNode): Promise<TreeNode[]> {
Expand All @@ -54,7 +66,7 @@ export class FileTree extends TreeImpl {
const uri = new URI(fileStat.uri);
const name = await this.labelProvider.getName(uri);
const icon = await this.labelProvider.getIcon(fileStat);
const id = fileStat.uri;
const id = this.generateFileTreeNodeId(name, fileStat.uri, parent);
const node = this.getNode(id);
if (fileStat.isDirectory) {
if (DirNode.is(node)) {
Expand All @@ -78,6 +90,38 @@ export class FileTree extends TreeImpl {
};
}

protected isVirtualRoot(node: CompositeTreeNode): boolean {
return node.parent === undefined && this.hasVirtualRoot && node.name === 'WorkspaceRoot';
}

protected generateFileTreeNodeId(nodeName: string, uri: string, parent: CompositeTreeNode | undefined): string {
if (parent && parent.name) {
return this.generateFileTreeNodeId(`${parent.name}/${nodeName}`, uri, parent.parent);
}
return `${nodeName}///${uri}`;
}

protected async resolveVirtualRootFileStat(virtualRoot: FileStatNode): Promise<FileStat | undefined> {
const rootFileStat = await this.fileSystem.getFileStat(virtualRoot.uri.toString());
const children = virtualRoot.fileStat.children;
let childrenFileStat: (FileStat | undefined)[] = [];
if (children) {
childrenFileStat = await Promise.all(children.map(child =>
this.fileSystem.getFileStat(child.uri.toString())
));
}
if (rootFileStat) {
rootFileStat.isDirectory = true;
rootFileStat.children = [];
for (const stat of childrenFileStat) {
if (stat) {
rootFileStat.children.push(stat);
}
}
}
return rootFileStat;

}
}

export interface FileStatNode extends SelectableTreeNode, UriSelection {
Expand All @@ -87,6 +131,13 @@ export namespace FileStatNode {
export function is(node: TreeNode | undefined): node is FileStatNode {
return !!node && 'fileStat' in node;
}

export function getUri(node: TreeNode | undefined): string {
if (!node) {
return '';
}
return node.id.slice(node.id.indexOf('file:///'));
}
}

export type FileNode = FileStatNode;
Expand Down Expand Up @@ -127,6 +178,23 @@ export namespace DirNode {
};
}

export function createWorkspaceRoot(workspace: FileStat, children: TreeNode[]): DirNode {
const id = workspace.uri;
const uri = new URI(id);
return {
id,
uri,
fileStat: workspace,
name: 'WorkspaceRoot',
parent: undefined,
children,
expanded: true,
selected: false,
focus: false,
visible: false
};
}

export function getContainingDir(node: TreeNode | undefined): DirNode | undefined {
let containing = node;
while (!!containing && !is(containing)) {
Expand Down
4 changes: 3 additions & 1 deletion packages/filesystem/src/browser/filesystem-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export class FileSystemListener implements FileSystemClient {
title: `The file '${file.uri}' has been changed on the file system.`,
msg: 'Do you want to overwrite the changes made on the file system?',
ok: 'Yes',
cancel: 'No'
cancel: 'No',
hasOk: true,
hasCancel: true
});
return dialog.open();
}
Expand Down
4 changes: 3 additions & 1 deletion packages/git/src/browser/git-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WorkingDirectoryStatus } from '../common/git-model';
import { GitRepositoryProvider } from './git-repository-provider';
import { GitFileChange, GitFileStatus } from '../common/git-model';
import { GitPreferences, GitConfiguration } from './git-preferences';
import { FileStatNode } from '@theia/filesystem/lib/browser';

@injectable()
export class GitDecorator implements TreeDecorator {
Expand Down Expand Up @@ -77,7 +78,8 @@ export class GitDecorator implements TreeDecorator {
return result;
}
const markers = this.appendContainerChanges(tree, status.changes);
for (const { id } of new DepthFirstTreeIterator(tree.root)) {
for (const treeNode of new DepthFirstTreeIterator(tree.root)) {
const id = FileStatNode.is(treeNode) ? FileStatNode.getUri(treeNode) : treeNode.id;
const marker = markers.get(id);
if (marker) {
result.set(id, marker);
Expand Down
Loading

0 comments on commit a98a351

Please sign in to comment.