Skip to content

Commit

Permalink
[wip] multi-root workspace support, minimal patch
Browse files Browse the repository at this point in the history
- part 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 37f2c75
Show file tree
Hide file tree
Showing 33 changed files with 842 additions and 190 deletions.
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
19 changes: 15 additions & 4 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 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/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
42 changes: 36 additions & 6 deletions packages/git/src/browser/git-repository-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import { Git, Repository } from '../common';
import { injectable, inject } from "inversify";
import { injectable, inject } from 'inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { Event, Emitter } from '@theia/core';
import { FileStat } from '@theia/filesystem/lib/common';

export interface GitRefreshOptions {
readonly maxCount: number
Expand All @@ -21,6 +22,9 @@ export class GitRepositoryProvider {
protected _allRepositories: Repository[] = [];
protected readonly onDidChangeRepositoryEmitter = new Emitter<Repository | undefined>();

protected root: FileStat | undefined;
protected roots: FileStat[];

constructor(
@inject(Git) protected readonly git: Git,
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService
Expand All @@ -29,6 +33,22 @@ export class GitRepositoryProvider {
}

protected async initialize(): Promise<void> {
const workspaceRootChangeData = await this.workspaceService.workspaceRootChange;
this.root = workspaceRootChangeData.root;
this.roots = workspaceRootChangeData.roots;
this.workspaceService.onWorkspaceRootChanged(async event => {
if (event) {
this.root = event.root;
this.roots = event.roots;
if (this.selectedRepository) {
const selected = this.roots.find(r => r.uri === this.selectedRepository!.localUri);
if (!selected) {
this.selectedRepository = undefined;
}
}
}
await this.refresh();
});
await this.refresh({ maxCount: 1 });
await this.refresh();
}
Expand Down Expand Up @@ -65,14 +85,24 @@ export class GitRepositoryProvider {
}

async refresh(options?: GitRefreshOptions): Promise<void> {
const root = await this.workspaceService.root;
if (!root) {
if (!this.root) {
return;
}
const repositories = await this.git.repositories(root.uri, {
...options
const repoUris = new Set<string>();
this._allRepositories = await Promise.all(
this.roots.map(r => this.git.repositories(r.uri, { ...options }))
).then(reposOfRoots => {
const repos: Repository[] = [];
reposOfRoots.forEach(reposPerRoot => {
reposPerRoot.forEach(repoOfOneRoot => {
if (!repoUris.has(repoOfOneRoot.localUri)) {
repos.push(repoOfOneRoot);
repoUris.add(repoOfOneRoot.localUri);
}
});
});
return repos;
});
this._allRepositories = repositories;
const selectedRepository = this._selectedRepository;
if (!selectedRepository || !this.exists(selectedRepository)) {
this.selectedRepository = this._allRepositories[0];
Expand Down
Loading

0 comments on commit 37f2c75

Please sign in to comment.