提交 4f33f591 编写于 作者: I Isidor Nikolic 提交者: GitHub

Merge pull request #29030 from Microsoft/isidorn/explorerModel

Isidorn/explorer model
......@@ -43,6 +43,11 @@ export interface IFileService {
*/
resolveFile(resource: URI, options?: IResolveFileOptions): TPromise<IFileStat>;
/**
* Same as resolveFile but supports resolving mulitple resources in parallel.
*/
resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): TPromise<IFileStat[]>;
/**
*Finds out if a file identified by the resource exists.
*/
......
......@@ -17,7 +17,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { FileStat } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat } from 'vs/workbench/parts/files/common/explorerModel';
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions';
import { copyFocusedFilesExplorerViewItem, revealInOSFocusedFilesExplorerItem, openFocusedExplorerItemSideBySideCommand, copyPathOfFocusedExplorerItem, copyPathCommand, revealInExplorerCommand, revealInOSCommand, openFolderPickerCommand, openWindowCommand, openFileInNewWindowCommand, deleteFocusedFilesExplorerViewItemCommand, moveFocusedFilesExplorerViewItemToTrashCommand, renameFocusedFilesExplorerViewItemCommand } from 'vs/workbench/parts/files/browser/fileCommands';
......
......@@ -27,7 +27,7 @@ import labels = require('vs/base/common/labels');
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService, IFileStat } from 'vs/platform/files/common/files';
import { toResource, IEditorIdentifier, EditorInput } from 'vs/workbench/common/editor';
import { FileStat, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel';
import { ExplorerView } from 'vs/workbench/parts/files/browser/views/explorerView';
import { ExplorerViewlet } from 'vs/workbench/parts/files/browser/explorerViewlet';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
......
......@@ -18,7 +18,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExplorerViewlet } from 'vs/workbench/parts/files/browser/explorerViewlet';
import { VIEWLET_ID, explorerItemToFileResource } from 'vs/workbench/parts/files/common/files';
import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerModel';
import errors = require('vs/base/common/errors');
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
......
......@@ -14,6 +14,7 @@ import labels = require('vs/base/common/labels');
import paths = require('vs/base/common/paths');
import { Action, IAction } from 'vs/base/common/actions';
import { prepareActions } from 'vs/workbench/browser/actions';
import { memoize } from 'vs/base/common/decorators';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocussedContext, ExplorerFocussedContext } from 'vs/workbench/parts/files/common/files';
......@@ -26,7 +27,7 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer
import * as DOM from 'vs/base/browser/dom';
import { CollapseAction } from 'vs/workbench/browser/viewlet';
import { CollapsibleView, IViewletViewOptions } from 'vs/workbench/parts/views/browser/views';
import { FileStat } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat, Model } from 'vs/workbench/parts/files/common/explorerModel';
import { IListService } from 'vs/platform/list/browser/listService';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
......@@ -119,7 +120,12 @@ export class ExplorerView extends CollapsibleView {
public renderHeader(container: HTMLElement): void {
const titleDiv = $('div.title').appendTo(container);
$('span').text(this.name).title(labels.getPathLabel(this.contextService.getWorkspace().resource.fsPath, void 0, this.environmentService)).appendTo(titleDiv);
const setHeader = () => {
const title = this.contextService.getWorkspace2().roots.map(root => labels.getPathLabel(root.fsPath, void 0, this.environmentService)).join();
$('span').text(this.name).title(title).appendTo(titleDiv);
};
this.toDispose.push(this.contextService.onDidChangeWorkspaceRoots(() => setHeader()));
setHeader();
super.renderHeader(container);
}
......@@ -140,6 +146,7 @@ export class ExplorerView extends CollapsibleView {
};
this.toDispose.push(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange));
this.toDispose.push(this.contextService.onDidChangeWorkspaceRoots(() => this.refreshFromEvent()));
onFileIconThemeChange(this.themeService.getFileIconTheme());
}
......@@ -300,7 +307,7 @@ export class ExplorerView extends CollapsibleView {
lastActiveFileResource = URI.parse(this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]);
}
if (lastActiveFileResource && this.root && this.root.find(lastActiveFileResource)) {
if (lastActiveFileResource && this.isCreated && this.model.findFirst(lastActiveFileResource)) {
this.editorService.openEditor({ resource: lastActiveFileResource, options: { revealIfVisible: true } }).done(null, errors.onUnexpectedError);
return refreshPromise;
......@@ -334,8 +341,13 @@ export class ExplorerView extends CollapsibleView {
return toResource(input, { supportSideBySide: true, filter: 'file' });
}
private get root(): FileStat {
return this.explorerViewer ? (<FileStat>this.explorerViewer.getInput()) : null;
private get isCreated(): boolean {
return !!(this.explorerViewer && this.explorerViewer.getInput());
}
@memoize
private get model(): Model {
return this.instantiationService.createInstance(Model);
}
public createViewer(container: Builder): ITree {
......@@ -405,36 +417,32 @@ export class ExplorerView extends CollapsibleView {
}
private onFileOperation(e: FileOperationEvent): void {
if (!this.root) {
if (!this.isCreated) {
return; // ignore if not yet created
}
let modelElement: FileStat;
let parent: FileStat;
let parentResource: URI;
let parentElement: FileStat;
// Add
if (e.operation === FileOperation.CREATE || e.operation === FileOperation.IMPORT || e.operation === FileOperation.COPY) {
const addedElement = e.target;
parentResource = URI.file(paths.dirname(addedElement.resource.fsPath));
parentElement = this.root.find(parentResource);
const parentResource = URI.file(paths.dirname(addedElement.resource.fsPath));
const parents = this.model.findAll(parentResource);
if (parentElement) {
if (parents.length) {
// Add the new file to its parent (Model)
const childElement = FileStat.create(addedElement);
parentElement.removeChild(childElement); // make sure to remove any previous version of the file if any
parentElement.addChild(childElement);
// Refresh the Parent (View)
this.explorerViewer.refresh(parentElement).then(() => {
return this.reveal(childElement, 0.5).then(() => {
// Focus new element
this.explorerViewer.setFocus(childElement);
});
}).done(null, errors.onUnexpectedError);
parents.forEach(p => {
const childElement = FileStat.create(addedElement, p.root);
p.removeChild(childElement); // make sure to remove any previous version of the file if any
p.addChild(childElement);
// Refresh the Parent (View)
this.explorerViewer.refresh(p).then(() => {
return this.reveal(childElement, 0.5).then(() => {
// Focus new element
this.explorerViewer.setFocus(childElement);
});
}).done(null, errors.onUnexpectedError);
});
}
}
......@@ -455,43 +463,39 @@ export class ExplorerView extends CollapsibleView {
// Handle Rename
if (oldParentResource && newParentResource && oldParentResource.toString() === newParentResource.toString()) {
modelElement = this.root.find(oldResource);
if (modelElement) {
const modelElements = this.model.findAll(oldResource);
modelElements.forEach(modelElement => {
// Rename File (Model)
modelElement.rename(newElement);
// Update Parent (View)
parent = modelElement.parent;
if (parent) {
this.explorerViewer.refresh(parent).done(() => {
// Select in Viewer if set
if (restoreFocus) {
this.explorerViewer.setFocus(modelElement);
}
}, errors.onUnexpectedError);
}
}
this.explorerViewer.refresh(modelElement.parent).done(() => {
// Select in Viewer if set
if (restoreFocus) {
this.explorerViewer.setFocus(modelElement);
}
}, errors.onUnexpectedError);
});
}
// Handle Move
else if (oldParentResource && newParentResource) {
const oldParent = this.root.find(oldParentResource);
const newParent = this.root.find(newParentResource);
modelElement = this.root.find(oldResource);
const newParents = this.model.findAll(newParentResource);
const modelElements = this.model.findAll(oldResource);
if (oldParent && newParent && modelElement) {
if (newParents.length && modelElements.length) {
// Move in Model
modelElement.move(newParent, (callback: () => void) => {
// Update old parent
this.explorerViewer.refresh(oldParent, true).done(callback, errors.onUnexpectedError);
}, () => {
// Update new parent
this.explorerViewer.refresh(newParent, true).done(() => this.explorerViewer.expand(newParent), errors.onUnexpectedError);
modelElements.forEach((modelElement, index) => {
const oldParent = modelElement.parent;
modelElement.move(newParents[index], (callback: () => void) => {
// Update old parent
this.explorerViewer.refresh(oldParent).done(callback, errors.onUnexpectedError);
}, () => {
// Update new parent
this.explorerViewer.refresh(newParents[index], true).done(() => this.explorerViewer.expand(newParents[index]), errors.onUnexpectedError);
});
});
}
}
......@@ -499,23 +503,24 @@ export class ExplorerView extends CollapsibleView {
// Delete
else if (e.operation === FileOperation.DELETE) {
modelElement = this.root.find(e.resource);
if (modelElement && modelElement.parent) {
parent = modelElement.parent;
// Remove Element from Parent (Model)
parent.removeChild(modelElement);
// Refresh Parent (View)
const restoreFocus = this.explorerViewer.isDOMFocused();
this.explorerViewer.refresh(parent).done(() => {
// Ensure viewer has keyboard focus if event originates from viewer
if (restoreFocus) {
this.explorerViewer.DOMFocus();
}
}, errors.onUnexpectedError);
}
const modelElements = this.model.findAll(e.resource);
modelElements.forEach(element => {
if (element.parent) {
const parent = element.parent;
// Remove Element from Parent (Model)
parent.removeChild(element);
// Refresh Parent (View)
const restoreFocus = this.explorerViewer.isDOMFocused();
this.explorerViewer.refresh(parent).done(() => {
// Ensure viewer has keyboard focus if event originates from viewer
if (restoreFocus) {
this.explorerViewer.DOMFocus();
}
}, errors.onUnexpectedError);
}
});
}
}
......@@ -561,7 +566,7 @@ export class ExplorerView extends CollapsibleView {
const added = e.getAdded();
const deleted = e.getDeleted();
if (!this.root) {
if (!this.isCreated) {
return false;
}
......@@ -582,8 +587,8 @@ export class ExplorerView extends CollapsibleView {
}
// Compute if parent is visible and added file not yet part of it
const parentStat = this.root.find(URI.file(parent));
if (parentStat && parentStat.isDirectoryResolved && !this.root.find(change.resource)) {
const parentStat = this.model.findFirst(URI.file(parent));
if (parentStat && parentStat.isDirectoryResolved && !this.model.findFirst(change.resource)) {
return true;
}
......@@ -600,7 +605,7 @@ export class ExplorerView extends CollapsibleView {
continue; // out of workspace file
}
if (this.root.find(del.resource)) {
if (this.model.findFirst(del.resource)) {
return true;
}
}
......@@ -673,7 +678,11 @@ export class ExplorerView extends CollapsibleView {
}
private doRefresh(): TPromise<void> {
const targetsToResolve: URI[] = [];
const targetsToResolve: { root: FileStat, resource: URI, options: { resolveTo: URI[] } }[] = [];
this.model.roots.forEach(root => {
const rootAndTargets = { root, resource: root.resource, options: { resolveTo: [] } };
targetsToResolve.push(rootAndTargets);
});
let targetsToExpand: URI[] = [];
if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) {
......@@ -681,51 +690,55 @@ export class ExplorerView extends CollapsibleView {
}
// First time refresh: Receive target through active editor input or selection and also include settings from previous session
if (!this.root) {
if (!this.isCreated) {
const activeFile = this.getActiveFile();
if (activeFile) {
targetsToResolve.push(activeFile);
const root = this.contextService.getRoot(activeFile);
if (root) {
const found = targetsToResolve.filter(t => t.root.resource.toString() === root.toString()).pop();
found.options.resolveTo.push(activeFile);
}
}
if (targetsToExpand.length) {
targetsToResolve.push(...targetsToExpand);
}
targetsToExpand.forEach(toExpand => {
const root = this.contextService.getRoot(toExpand);
if (root) {
const found = targetsToResolve.filter(ttr => ttr.resource.toString() === root.toString()).pop();
found.options.resolveTo.push(toExpand);
}
});
}
// Subsequent refresh: Receive targets through expanded folders in tree
else {
this.getResolvedDirectories(this.root, targetsToResolve);
targetsToResolve.forEach(t => {
this.getResolvedDirectories(t.root, t.options.resolveTo);
});
}
// Load Root Stat with given target path configured
const options: IResolveFileOptions = { resolveTo: targetsToResolve };
const promise = this.fileService.resolveFile(this.contextService.getWorkspace().resource, options).then(stat => {
let explorerPromise: TPromise<void>;
const promise = this.fileService.resolveFiles(targetsToResolve).then(stats => {
// Convert to model
const modelStat = FileStat.create(stat, options.resolveTo);
// First time refresh: The stat becomes the input of the viewer
if (!this.root) {
explorerPromise = this.explorerViewer.setInput(modelStat).then(() => {
// Make sure to expand all folders that where expanded in the previous session
if (targetsToExpand) {
return this.explorerViewer.expandAll(targetsToExpand.map(expand => this.root.find(expand)));
}
const modelStats = stats.map((stat, index) => FileStat.create(stat, targetsToResolve[index].root.resource, targetsToResolve[index].options.resolveTo));
// Subsequent refresh: Merge stat into our local model and refresh tree
modelStats.forEach((modelStat, index) => FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index]));
return TPromise.as(null);
});
const input = this.model.roots.length === 1 ? this.model.roots[0] : this.model;
if (input === this.explorerViewer.getInput()) {
return this.explorerViewer.refresh();
}
// Subsequent refresh: Merge stat into our local model and refresh tree
else {
FileStat.mergeLocalWithDisk(modelStat, this.root);
// First time refresh: The stat becomes the input of the viewer
// Display roots only when there is more than 1 root
return this.explorerViewer.setInput(input).then(() => {
explorerPromise = this.explorerViewer.refresh(this.root);
}
// Make sure to expand all folders that where expanded in the previous session
if (targetsToExpand) {
return this.explorerViewer.expandAll(targetsToExpand.map(expand => this.model.findFirst(expand)));
}
return explorerPromise;
return TPromise.as(null);
});
}, (e: any) => TPromise.wrapError(e));
this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
......@@ -778,27 +791,28 @@ export class ExplorerView extends CollapsibleView {
}
// First try to get the stat object from the input to avoid a roundtrip
if (!this.root) {
if (!this.isCreated) {
return TPromise.as(null);
}
const fileStat = this.root.find(resource);
const fileStat = this.model.findFirst(resource);
if (fileStat) {
return this.doSelect(fileStat, reveal);
}
// Stat needs to be resolved first and then revealed
const options: IResolveFileOptions = { resolveTo: [resource] };
return this.fileService.resolveFile(this.contextService.getWorkspace().resource, options).then(stat => {
const rootUri = this.contextService.getRoot(resource) || this.model.roots[0].resource;
return this.fileService.resolveFile(rootUri, options).then(stat => {
// Convert to model
const modelStat = FileStat.create(stat, options.resolveTo);
const modelStat = FileStat.create(stat, rootUri, options.resolveTo);
const root = this.model.roots.filter(r => r.resource.toString() === rootUri.toString()).pop();
// Update Input with disk Stat
FileStat.mergeLocalWithDisk(modelStat, this.root);
FileStat.mergeLocalWithDisk(modelStat, root);
// Select and Reveal
return this.explorerViewer.refresh(this.root).then(() => this.doSelect(this.root.find(resource), reveal));
return this.explorerViewer.refresh(root).then(() => this.doSelect(root.find(resource), reveal));
}, (e: any) => this.messageService.show(Severity.Error, e));
}
......@@ -849,7 +863,7 @@ export class ExplorerView extends CollapsibleView {
public shutdown(): void {
// Keep list of expanded folders to restore on next load
if (this.root) {
if (this.isCreated) {
const expanded = this.explorerViewer.getExpandedElements()
.filter((e: FileStat) => e.resource.toString() !== this.contextService.getWorkspace().resource.toString())
.map((e: FileStat) => e.resource.toString());
......
......@@ -31,7 +31,7 @@ import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState
import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDrop, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree';
import { DesktopDragAndDropData, ExternalElementsDragAndDropData } from 'vs/base/parts/tree/browser/treeDnd';
import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
import { FileStat, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat, NewStatPlaceholder, Model } from 'vs/workbench/parts/files/common/explorerModel';
import { DragMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
......@@ -60,15 +60,22 @@ export class FileDataSource implements IDataSource {
@IWorkspaceContextService private contextService: IWorkspaceContextService
) { }
public getId(tree: ITree, stat: FileStat): string {
return stat.getId();
public getId(tree: ITree, stat: FileStat | Model): string {
if (stat instanceof Model) {
return 'model';
}
return `${stat.root.toString()}:${stat.getId()}`;
}
public hasChildren(tree: ITree, stat: FileStat): boolean {
return stat.isDirectory;
public hasChildren(tree: ITree, stat: FileStat | Model): boolean {
return stat instanceof Model || (stat instanceof FileStat && stat.isDirectory);
}
public getChildren(tree: ITree, stat: FileStat): TPromise<FileStat[]> {
public getChildren(tree: ITree, stat: FileStat | Model): TPromise<FileStat[]> {
if (stat instanceof Model) {
return TPromise.as(stat.roots);
}
// Return early if stat is already resolved
if (stat.isDirectoryResolved) {
......@@ -82,7 +89,7 @@ export class FileDataSource implements IDataSource {
const promise = this.fileService.resolveFile(stat.resource, { resolveSingleChildDescendants: true }).then(dirStat => {
// Convert to view model
const modelDirStat = FileStat.create(dirStat);
const modelDirStat = FileStat.create(dirStat, stat.root);
// Add children to folder
for (let i = 0; i < modelDirStat.children.length; i++) {
......@@ -104,19 +111,18 @@ export class FileDataSource implements IDataSource {
}
}
public getParent(tree: ITree, stat: FileStat): TPromise<FileStat> {
public getParent(tree: ITree, stat: FileStat | Model): TPromise<FileStat> {
if (!stat) {
return TPromise.as(null); // can be null if nothing selected in the tree
}
// Return if root reached
const workspace = this.contextService.getWorkspace();
if (workspace && stat.resource.toString() === workspace.resource.toString()) {
if (tree.getInput() === stat) {
return TPromise.as(null);
}
// Return if parent already resolved
if (stat.parent) {
if (stat instanceof FileStat && stat.parent) {
return TPromise.as(stat.parent);
}
......@@ -404,7 +410,7 @@ export class FileController extends DefaultController {
this.state = state;
}
public onLeftClick(tree: ITree, stat: FileStat, event: IMouseEvent, origin: string = 'mouse'): boolean {
public onLeftClick(tree: ITree, stat: FileStat | Model, event: IMouseEvent, origin: string = 'mouse'): boolean {
const payload = { origin: origin };
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
......@@ -421,8 +427,7 @@ export class FileController extends DefaultController {
}
// Handle root
const workspace = this.contextService.getWorkspace();
if (workspace && stat.resource.toString() === workspace.resource.toString()) {
if (stat instanceof Model) {
tree.clearFocus(payload);
tree.clearSelection(payload);
......@@ -539,6 +544,15 @@ export class FileSorter implements ISorter {
return 1;
}
// Do not sort roots
if (statA.isRoot) {
return -1;
}
if (statB.isRoot) {
return 1;
}
return comparers.compareFileNames(statA.name, statB.name);
}
}
......@@ -558,6 +572,7 @@ export class FileFilter implements IFilter {
const excludesConfig = (configuration && configuration.files && configuration.files.exclude) || Object.create(null);
const needsRefresh = !objects.equals(this.hiddenExpression, excludesConfig);
// This needs to be per folder
this.hiddenExpression = objects.clone(excludesConfig); // do not keep the config, as it gets mutated under our hoods
return needsRefresh;
......@@ -619,6 +634,9 @@ export class FileDragAndDrop implements IDragAndDrop {
}
public getDragURI(tree: ITree, stat: FileStat): string {
if (this.contextService.getWorkspace2().roots.some(r => r.toString() === stat.resource.toString())) {
return null; // Can not move root folder
}
if (stat.isDirectory) {
return URI.from({ scheme: 'folder', path: stat.resource.fsPath }).toString(); // indicates that we are dragging a folder
}
......@@ -656,8 +674,8 @@ export class FileDragAndDrop implements IDragAndDrop {
}
}
public onDragOver(tree: ITree, data: IDragAndDropData, target: FileStat, originalEvent: DragMouseEvent): IDragOverReaction {
if (!this.dropEnabled) {
public onDragOver(tree: ITree, data: IDragAndDropData, target: FileStat | Model, originalEvent: DragMouseEvent): IDragOverReaction {
if (!this.dropEnabled || target instanceof Model) {
return DRAG_OVER_REJECT;
}
......@@ -719,8 +737,8 @@ export class FileDragAndDrop implements IDragAndDrop {
return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true);
}
const workspace = this.contextService.getWorkspace();
if (workspace && target.resource.toString() !== workspace.resource.toString()) {
const workspace = this.contextService.getWorkspace2();
if (workspace && workspace.roots.every(r => r.toString() !== target.resource.toString())) {
return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_UP_COPY : DRAG_OVER_ACCEPT_BUBBLE_UP;
}
......
......@@ -22,7 +22,7 @@ import { CollapsibleView, IViewletViewOptions } from 'vs/workbench/parts/views/b
import { IFilesConfiguration, VIEWLET_ID, OpenEditorsFocussedContext, ExplorerFocussedContext } from 'vs/workbench/parts/files/common/files';
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { OpenEditor } from 'vs/workbench/parts/files/common/explorerViewModel';
import { OpenEditor } from 'vs/workbench/parts/files/common/explorerModel';
import { Renderer, DataSource, Controller, AccessibilityProvider, ActionProvider, DragAndDrop } from 'vs/workbench/parts/files/browser/views/openEditorsViewer';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { CloseAllEditorsAction } from 'vs/workbench/browser/parts/editor/editorActions';
......
......@@ -22,7 +22,7 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IEditorGroup, IEditorStacksModel } from 'vs/workbench/common/editor';
import { OpenEditor } from 'vs/workbench/parts/files/common/explorerViewModel';
import { OpenEditor } from 'vs/workbench/parts/files/common/explorerModel';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { explorerItemToFileResource } from 'vs/workbench/parts/files/common/files';
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
......
......@@ -7,11 +7,12 @@
import URI from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import { ResourceMap } from 'vs/base/common/map';
import { isLinux } from 'vs/base/common/platform';
import { IFileStat, isParent } from 'vs/platform/files/common/files';
import { IEditorInput } from 'vs/platform/editor/common/editor';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorGroup, toResource } from 'vs/workbench/common/editor';
import { ResourceMap } from 'vs/base/common/map';
import { isLinux } from 'vs/base/common/platform';
export enum StatType {
FILE,
......@@ -19,6 +20,41 @@ export enum StatType {
ANY
}
export class Model {
private _roots: FileStat[];
constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
const setRoots = () => this._roots = this.contextService.getWorkspace2().roots.map(uri => new FileStat(uri, uri));
this.contextService.onDidChangeWorkspaceRoots(() => setRoots());
setRoots();
}
public get roots(): FileStat[] {
return this._roots;
}
/**
* Returns a child stat from this stat that matches with the provided path.
* Starts matching from the first root.
* Will return "null" in case the child does not exist.
*/
public findAll(resource: URI): FileStat[] {
return this.roots.map(root => root.find(resource)).filter(stat => !!stat);
}
public findFirst(resource: URI): FileStat {
for (let root of this.roots) {
const result = root.find(resource);
if (result) {
return result;
}
}
return null;
}
}
export class FileStat implements IFileStat {
public resource: URI;
public name: string;
......@@ -31,7 +67,7 @@ export class FileStat implements IFileStat {
public isDirectoryResolved: boolean;
constructor(resource: URI, isDirectory?: boolean, hasChildren?: boolean, name: string = paths.basename(resource.fsPath), mtime?: number, etag?: string) {
constructor(resource: URI, public root: URI, isDirectory?: boolean, hasChildren?: boolean, name: string = paths.basename(resource.fsPath), mtime?: number, etag?: string) {
this.resource = resource;
this.name = name;
this.isDirectory = !!isDirectory;
......@@ -51,8 +87,12 @@ export class FileStat implements IFileStat {
return this.resource.toString();
}
public static create(raw: IFileStat, resolveTo?: URI[]): FileStat {
const stat = new FileStat(raw.resource, raw.isDirectory, raw.hasChildren, raw.name, raw.mtime, raw.etag);
public get isRoot(): boolean {
return this.resource.toString() === this.root.toString();
}
public static create(raw: IFileStat, root: URI, resolveTo?: URI[]): FileStat {
const stat = new FileStat(raw.resource, root, raw.isDirectory, raw.hasChildren, raw.name, raw.mtime, raw.etag);
// Recursively add children if present
if (stat.isDirectory) {
......@@ -67,7 +107,7 @@ export class FileStat implements IFileStat {
// Recurse into children
if (raw.children) {
for (let i = 0, len = raw.children.length; i < len; i++) {
const child = FileStat.create(raw.children[i], resolveTo);
const child = FileStat.create(raw.children[i], root, resolveTo);
child.parent = stat;
stat.children.push(child);
stat.hasChildren = stat.children.length > 0;
......@@ -107,9 +147,11 @@ export class FileStat implements IFileStat {
// Map resource => stat
const oldLocalChildren = new ResourceMap<FileStat>();
local.children.forEach((localChild: FileStat) => {
oldLocalChildren.set(localChild.resource, localChild);
});
if (local.children) {
local.children.forEach((localChild: FileStat) => {
oldLocalChildren.set(localChild.resource, localChild);
});
}
// Clear current children
local.children = [];
......@@ -275,7 +317,7 @@ export class NewStatPlaceholder extends FileStat {
private directoryPlaceholder: boolean;
constructor(isDirectory: boolean) {
super(URI.file(''));
super(URI.file(''), URI.file(''));
this.id = NewStatPlaceholder.ID++;
this.isDirectoryResolved = isDirectory;
......
......@@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerModel';
import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
/**
......
......@@ -11,10 +11,10 @@ import { isLinux, isWindows } from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
import { join } from 'vs/base/common/paths';
import { validateFileName } from 'vs/workbench/parts/files/browser/fileActions';
import { FileStat } from 'vs/workbench/parts/files/common/explorerViewModel';
import { FileStat } from 'vs/workbench/parts/files/common/explorerModel';
function createStat(path, name, isFolder, hasChildren, size, mtime) {
return new FileStat(toResource(path), isFolder, hasChildren, name, mtime);
return new FileStat(toResource(path), toResource(path), isFolder, hasChildren, name, mtime);
}
function toResource(path) {
......@@ -246,20 +246,20 @@ suite('Files - View Model', () => {
test('Merge Local with Disk', function () {
const d = new Date().toUTCString();
const merge1 = new FileStat(URI.file(join('C:\\', '/path/to')), true, false, 'to', Date.now(), d);
const merge2 = new FileStat(URI.file(join('C:\\', '/path/to')), true, false, 'to', Date.now(), new Date(0).toUTCString());
const merge1 = new FileStat(URI.file(join('C:\\', '/path/to')), URI.file(join('C:\\', '/path')), true, false, 'to', Date.now(), d);
const merge2 = new FileStat(URI.file(join('C:\\', '/path/to')), URI.file(join('C:\\', '/path')), true, false, 'to', Date.now(), new Date(0).toUTCString());
// Merge Properties
FileStat.mergeLocalWithDisk(merge2, merge1);
assert.strictEqual(merge1.mtime, merge2.mtime);
// Merge Child when isDirectoryResolved=false is a no-op
merge2.addChild(new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), true, false, 'foo.html', Date.now(), d));
merge2.addChild(new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), URI.file(join('C:\\', '/path')), true, false, 'foo.html', Date.now(), d));
FileStat.mergeLocalWithDisk(merge2, merge1);
assert.strictEqual(merge1.children.length, 0);
// Merge Child with isDirectoryResolved=true
const child = new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), true, false, 'foo.html', Date.now(), d);
const child = new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), URI.file(join('C:\\', '/path')), true, false, 'foo.html', Date.now(), d);
merge2.removeChild(child);
merge2.addChild(child);
merge2.isDirectoryResolved = true;
......
......@@ -642,4 +642,4 @@ class Configuration<T> extends BaseConfiguration<T> {
return true;
}
}
\ No newline at end of file
}
......@@ -198,6 +198,10 @@ export class FileService implements IFileService {
return this.raw.resolveFile(resource, options);
}
public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IFileStat[]> {
return this.raw.resolveFiles(toResolve);
}
public existsFile(resource: uri): TPromise<boolean> {
return this.raw.existsFile(resource);
}
......
......@@ -156,6 +156,10 @@ export class FileService implements IFileService {
return this.resolve(resource, options);
}
public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IFileStat[]> {
return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)));
}
public existsFile(resource: uri): TPromise<boolean> {
return this.resolveFile(resource).then(() => true, () => false);
}
......
......@@ -670,6 +670,10 @@ export class TestFileService implements IFileService {
});
}
resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): TPromise<IFileStat[]> {
return TPromise.join(toResolve.map(resourceAndOption => this.resolveFile(resourceAndOption.resource, resourceAndOption.options)));
}
existsFile(resource: URI): TPromise<boolean> {
return TPromise.as(null);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册