From df9bf3e59d6d0246ac337dd3068131b0bc86800f Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 13 Dec 2018 17:01:59 +0100 Subject: [PATCH 01/65] explorer use the new tree --- .../browser/parts/views/viewsViewlet.ts | 70 - src/vs/workbench/parts/files/common/files.ts | 2 +- .../files/electron-browser/explorerViewlet.ts | 19 +- .../files/electron-browser/fileActions.ts | 132 +- .../electron-browser/views/explorerView.ts | 476 +++-- .../electron-browser/views/explorerViewer.ts | 1535 ++++++++--------- 6 files changed, 981 insertions(+), 1253 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index cd256a1288f..767e16d82df 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -30,76 +30,6 @@ import { localize } from 'vs/nls'; import { IAddedViewDescriptorRef, IViewDescriptorRef, PersistentContributableViewsModel } from 'vs/workbench/browser/parts/views/views'; import { Registry } from 'vs/platform/registry/common/platform'; -export abstract class TreeViewsViewletPanel extends ViewletPanel { - - protected tree: WorkbenchTree; - - setExpanded(expanded: boolean): void { - if (this.isExpanded() !== expanded) { - this.updateTreeVisibility(this.tree, expanded); - super.setExpanded(expanded); - } - } - - setVisible(visible: boolean): void { - if (this.isVisible() !== visible) { - super.setVisible(visible); - this.updateTreeVisibility(this.tree, visible && this.isExpanded()); - } - } - - focus(): void { - super.focus(); - this.focusTree(); - } - - layoutBody(size: number): void { - if (this.tree) { - this.tree.layout(size); - } - } - - protected updateTreeVisibility(tree: WorkbenchTree, isVisible: boolean): void { - if (!tree) { - return; - } - - if (isVisible) { - DOM.show(tree.getHTMLElement()); - } else { - DOM.hide(tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it - } - - if (isVisible) { - tree.onVisible(); - } else { - tree.onHidden(); - } - } - - private focusTree(): void { - if (!this.tree) { - return; // return early if viewlet has not yet been created - } - - // Make sure the current selected element is revealed - const selectedElement = this.tree.getSelection()[0]; - if (selectedElement) { - this.tree.reveal(selectedElement); - } - - // Pass Focus to Viewer - this.tree.domFocus(); - } - - dispose(): void { - if (this.tree) { - this.tree.dispose(); - } - super.dispose(); - } -} - export interface IViewletViewOptions extends IViewletPanelOptions { viewletState: object; } diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 36660df4223..03061cbd7e2 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -36,7 +36,7 @@ export interface IExplorerViewlet extends IViewlet { } export interface IExplorerView { - select(resource: URI, reveal?: boolean): TPromise; + select(resource: URI, reveal?: boolean): void; } /** diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index c7f73dc9eab..fb16a1dea62 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -5,13 +5,11 @@ import 'vs/css!./media/explorerviewlet'; import { localize } from 'vs/nls'; -import { IActionRunner } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, IExplorerViewlet, VIEW_CONTAINER } from 'vs/workbench/parts/files/common/files'; import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { ActionRunner, FileViewletState } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; -import { ExplorerView, IExplorerViewOptions } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; +import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; import { EmptyView } from 'vs/workbench/parts/files/electron-browser/views/emptyView'; import { OpenEditorsView } from 'vs/workbench/parts/files/electron-browser/views/openEditorsView'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -150,7 +148,6 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi private static readonly EXPLORER_VIEWS_STATE = 'workbench.explorer.views.state'; - private fileViewletState: FileViewletState; private viewletVisibleContextKey: IContextKey; constructor( @@ -169,7 +166,6 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi ) { super(VIEWLET_ID, ExplorerViewlet.EXPLORER_VIEWS_STATE, true, configurationService, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); - this.fileViewletState = new FileViewletState(); this.viewletVisibleContextKey = ExplorerViewletVisibleContext.bindTo(contextKeyService); this._register(this.contextService.onDidChangeWorkspaceName(e => this.updateTitleArea())); @@ -217,7 +213,7 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi }); const explorerInstantiator = this.instantiationService.createChild(new ServiceCollection([IEditorService, delegatingEditorService])); - return explorerInstantiator.createInstance(ExplorerView, { ...options, fileViewletState: this.fileViewletState }); + return explorerInstantiator.createInstance(ExplorerView, options); } return super.createView(viewDescriptor, options); } @@ -239,17 +235,6 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi super.setVisible(visible); } - public getActionRunner(): IActionRunner { - if (!this.actionRunner) { - this.actionRunner = new ActionRunner(this.fileViewletState); - } - return this.actionRunner; - } - - public getViewletState(): FileViewletState { - return this.fileViewletState; - } - focus(): void { const explorerView = this.getExplorerView(); if (explorerView && explorerView.isExpanded()) { diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index a261d9c43dc..c9b164bd8a8 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -26,10 +26,9 @@ import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/file import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IInstantiationService, ServicesAccessor, IConstructorSignature2 } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, IConstructorSignature3 } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; @@ -49,6 +48,8 @@ import { Constants } from 'vs/editor/common/core/uint'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; +import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { EditableExplorerItems } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; export interface IEditableData { action: IAction; @@ -307,16 +308,14 @@ class RenameFileAction extends BaseRenameAction { /* Base New File/Folder Action */ export class BaseNewAction extends BaseFileAction { private presetFolder: ExplorerItem; - private tree: ITree; - private isFile: boolean; - private renameAction: BaseRenameAction; constructor( id: string, label: string, - tree: ITree, - isFile: boolean, - editableAction: BaseRenameAction, + private tree: AsyncDataTree, + private viewletState: IFileViewletState, + private isFile: boolean, + private renameAction: BaseRenameAction, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @@ -327,30 +326,19 @@ export class BaseNewAction extends BaseFileAction { if (element) { this.presetFolder = element.isDirectory ? element : element.parent; } - - this.tree = tree; - this.isFile = isFile; - this.renameAction = editableAction; } public run(context?: any): TPromise { - if (!context) { - return Promise.reject(new Error('No context provided to BaseNewAction.')); - } - - const viewletState = context.viewletState; - if (!viewletState) { - return Promise.reject(new Error('Invalid viewlet state provided to BaseNewAction.')); - } let folder = this.presetFolder; if (!folder) { - const focus = this.tree.getFocus(); + const focusedElements = this.tree.getFocus(); + const focus = focusedElements.length ? focusedElements[0] : undefined; + if (focus) { folder = focus.isDirectory ? focus : focus.parent; } else { - const input: ExplorerItem | Model = this.tree.getInput(); - folder = input instanceof Model ? input.roots[0] : input; + folder = this.tree.getNode(null).element; } } @@ -365,44 +353,31 @@ export class BaseNewAction extends BaseFileAction { return Promise.resolve(new Error('Parent folder is already in the process of creating a file')); } - return this.tree.reveal(folder, 0.5).then(() => { - return this.tree.expand(folder).then(() => { - const stat = NewStatPlaceholder.addNewStatPlaceholder(folder, !this.isFile); + this.tree.reveal(folder, 0.5); + return this.tree.expand(folder).then(() => { + const stat = NewStatPlaceholder.addNewStatPlaceholder(folder, !this.isFile); - this.renameAction.element = stat; + this.renameAction.element = stat; - viewletState.setEditable(stat, { - action: this.renameAction, - validator: (value) => { - const message = this.renameAction.validateFileName(folder, value); + this.viewletState.setEditable(stat, { + action: this.renameAction, + validator: (value) => { + const message = this.renameAction.validateFileName(folder, value); - if (!message) { - return null; - } - - return { - content: message, - formatContent: true, - type: MessageType.ERROR - }; + if (!message) { + return null; } - }); - return this.tree.refresh(folder).then(() => { - return this.tree.expand(folder).then(() => { - return this.tree.reveal(stat, 0.5).then(() => { - this.tree.setHighlight(stat); + return { + content: message, + formatContent: true, + type: MessageType.ERROR + }; + } + }); - const unbind = this.tree.onDidChangeHighlight((e: IHighlightEvent) => { - if (!e.highlight) { - stat.destroy(); - this.tree.refresh(folder); - unbind.dispose(); - } - }); - }); - }); - }); + return this.tree.refresh(folder).then(() => { + return this.tree.expand(folder).then(() => this.tree.reveal(stat, 0.5)); }); }); } @@ -412,14 +387,15 @@ export class BaseNewAction extends BaseFileAction { export class NewFileAction extends BaseNewAction { constructor( - tree: ITree, + tree: AsyncDataTree, + fileViewletState: EditableExplorerItems, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, @IInstantiationService instantiationService: IInstantiationService ) { - super('explorer.newFile', NEW_FILE_LABEL, tree, true, instantiationService.createInstance(CreateFileAction, element), null, fileService, notificationService, textFileService); + super('explorer.newFile', NEW_FILE_LABEL, tree, fileViewletState, true, instantiationService.createInstance(CreateFileAction, element), null, fileService, notificationService, textFileService); this.class = 'explorer-action new-file'; this._updateEnablement(); @@ -430,14 +406,15 @@ export class NewFileAction extends BaseNewAction { export class NewFolderAction extends BaseNewAction { constructor( - tree: ITree, + tree: AsyncDataTree, + fileViewletState: EditableExplorerItems, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, @IInstantiationService instantiationService: IInstantiationService ) { - super('explorer.newFolder', NEW_FOLDER_LABEL, tree, false, instantiationService.createInstance(CreateFolderAction, element), null, fileService, notificationService, textFileService); + super('explorer.newFolder', NEW_FOLDER_LABEL, tree, fileViewletState, false, instantiationService.createInstance(CreateFolderAction, element), null, fileService, notificationService, textFileService); this.class = 'explorer-action new-folder'; this._updateEnablement(); @@ -1315,7 +1292,7 @@ export class FocusFilesExplorer extends Action { const view = viewlet.getExplorerView(); if (view) { view.setExpanded(true); - view.getViewer().domFocus(); + view.focus(); } }); } @@ -1365,12 +1342,7 @@ export class CollapseExplorerView extends Action { return this.viewletService.openViewlet(VIEWLET_ID, true).then((viewlet: ExplorerViewlet) => { const explorerView = viewlet.getExplorerView(); if (explorerView) { - const viewer = explorerView.getViewer(); - if (viewer) { - const action = new CollapseAction(viewer, true, null); - action.run(); - action.dispose(); - } + explorerView.collapseAll(); } }); } @@ -1554,24 +1526,24 @@ class ClipboardContentProvider implements ITextModelContentProvider { } interface IExplorerContext { - viewletState: IFileViewletState; stat: ExplorerItem; selection: ExplorerItem[]; } -function getContext(listWidget: ListWidget, viewletService: IViewletService): IExplorerContext { +function getContext(listWidget: ListWidget): IExplorerContext { // These commands can only be triggered when explorer viewlet is visible so get it using the active viewlet - const tree = listWidget; - const stat = tree.getFocus(); + const tree = >listWidget; + const focus = tree.getFocus(); + const stat = focus.length ? focus[0] : undefined; const selection = tree.getSelection(); // Only respect the selection if user clicked inside it (focus belongs to it) - return { stat, selection: selection && selection.indexOf(stat) >= 0 ? selection : [], viewletState: (viewletService.getActiveViewlet()).getViewletState() }; + return { stat, selection: selection && selection.indexOf(stat) >= 0 ? selection : [] }; } // TODO@isidor these commands are calling into actions due to the complex inheritance action structure. // It should be the other way around, that actions call into commands. -function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature2): TPromise { +function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature3, EditableExplorerItems, ExplorerItem, Action>): TPromise { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); const viewletService = accessor.get(IViewletService); @@ -1585,10 +1557,10 @@ function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: ICons const explorerView = explorer.getExplorerView(); if (explorerView && explorerView.isVisible() && explorerView.isExpanded()) { explorerView.focus(); - const explorerContext = getContext(listService.lastFocusedList, viewletService); - const action = instantationService.createInstance(constructor, listService.lastFocusedList, explorerContext.stat); + const { stat } = getContext(listService.lastFocusedList); + const action = instantationService.createInstance(constructor, listService.lastFocusedList, explorerView.editableExplorerItems, stat); - return action.run(explorerContext); + return action.run(); } return undefined; @@ -1612,7 +1584,7 @@ CommandsRegistry.registerCommand({ export const renameHandler = (accessor: ServicesAccessor) => { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); - const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); + const explorerContext = getContext(listService.lastFocusedList); const renameAction = instantationService.createInstance(TriggerRenameFileAction, listService.lastFocusedList, explorerContext.stat); return renameAction.run(explorerContext); @@ -1621,7 +1593,7 @@ export const renameHandler = (accessor: ServicesAccessor) => { export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); - const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); + const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; const moveFileToTrashAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, true); @@ -1631,7 +1603,7 @@ export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { export const deleteFileHandler = (accessor: ServicesAccessor) => { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); - const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); + const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; const deleteFileAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, false); @@ -1641,7 +1613,7 @@ export const deleteFileHandler = (accessor: ServicesAccessor) => { export const copyFileHandler = (accessor: ServicesAccessor) => { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); - const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); + const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; const copyFileAction = instantationService.createInstance(CopyFileAction, listService.lastFocusedList, stats); @@ -1652,7 +1624,7 @@ export const pasteFileHandler = (accessor: ServicesAccessor) => { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); const clipboardService = accessor.get(IClipboardService); - const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); + const explorerContext = getContext(listService.lastFocusedList); return Promise.all(resources.distinctParents(clipboardService.readResources(), r => r).map(toCopy => { const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index c3fbbc65e91..d4faa531a5d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; -import { ThrottledDelayer, Delayer } from 'vs/base/common/async'; +import { ThrottledDelayer } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import * as glob from 'vs/base/common/glob'; @@ -15,14 +15,13 @@ import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext } from 'vs/workbench/parts/files/common/files'; import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileService, FILES_EXCLUDE_CONFIG, IFileStat } from 'vs/platform/files/common/files'; -import { RefreshViewExplorerAction, NewFolderAction, NewFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; -import { FileDragAndDrop, FileFilter, FileSorter, FileController, FileRenderer, FileDataSource, FileViewletState, FileAccessibilityProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { RefreshViewExplorerAction, NewFolderAction, NewFileAction, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as DOM from 'vs/base/browser/dom'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; -import { TreeViewsViewletPanel, FileIconThemableWorkbenchTree, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { ExplorerItem, Model } from 'vs/workbench/parts/files/common/explorerModel'; +import { CollapseAction2 } from 'vs/workbench/browser/viewlet'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { ExplorerDecorationsProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -36,32 +35,37 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; import { isLinux } from 'vs/base/common/platform'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, EditableExplorerItems as EditableExplorerItems, FilesFilter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export interface IExplorerViewOptions extends IViewletViewOptions { - fileViewletState: FileViewletState; + fileViewletState: EditableExplorerItems; } -export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView { +export class ExplorerView extends ViewletPanel implements IExplorerView { public static readonly ID: string = 'workbench.explorer.fileView'; private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes - private static readonly MEMENTO_LAST_ACTIVE_FILE_RESOURCE = 'explorer.memento.lastActiveFileResource'; - private static readonly MEMENTO_EXPANDED_FOLDER_RESOURCES = 'explorer.memento.expandedFolderResources'; - public readonly id: string = ExplorerView.ID; - private explorerViewer: WorkbenchTree; - private filter: FileFilter; - private fileViewletState: FileViewletState; + private tree: WorkbenchAsyncDataTree; + private filter: FilesFilter; + private isCreated: boolean; + private _editableExplorerItems: EditableExplorerItems; private explorerRefreshDelayer: ThrottledDelayer; @@ -75,8 +79,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView private shouldRefresh: boolean; private autoReveal: boolean; private sortOrder: SortOrder; - private viewState: object; - private treeContainer: HTMLElement; private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private isDisposed: boolean; @@ -92,15 +94,18 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView @IFileService private fileService: IFileService, @IPartService private partService: IPartService, @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IDecorationsService decorationService: IDecorationsService, - @ILabelService private labelService: ILabelService + @ILabelService private labelService: ILabelService, + @IThemeService private themeService: IWorkbenchThemeService, + @IListService private listService: IListService, + @IMenuService private menuService: IMenuService, + @IClipboardService private clipboardService: IClipboardService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); - this.viewState = options.viewletState; - this.fileViewletState = options.fileViewletState; + this._editableExplorerItems = new EditableExplorerItems(); this.autoReveal = true; this.explorerRefreshDelayer = new ThrottledDelayer(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY); @@ -149,24 +154,27 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView setHeader(); } - public get name(): string { + protected layoutBody(size: number): void { + this.tree.layout(size); + } + + get name(): string { return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace()); } - public get title(): string { + get title(): string { return this.name; } - public set title(value: string) { + set title(value: string) { // noop } - public set name(value) { - // noop + get editableExplorerItems(): EditableExplorerItems { + return this._editableExplorerItems; } - public render(): void { - + render(): void { super.render(); // Update configuration @@ -174,11 +182,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView this.onConfigurationUpdated(configuration); // Load and Fill Viewer - let targetsToExpand: URI[] = []; - if (this.viewState[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) { - targetsToExpand = this.viewState[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e)); - } - this.doRefresh(targetsToExpand).then(() => { + this.doRefresh().then(() => { // When the explorer viewer is loaded, listen to changes to the editor input this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.revealActiveFile())); @@ -190,16 +194,16 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }); } - public renderBody(container: HTMLElement): void { - this.treeContainer = DOM.append(container, DOM.$('.explorer-folders-view')); - this.tree = this.createViewer(this.treeContainer); + renderBody(container: HTMLElement): void { + const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view')); + this.createTree(treeContainer); if (this.toolbar) { this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); } this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.refreshFromEvent(e.added))); - this.disposables.push(this.contextService.onDidChangeWorkbenchState(e => this.refreshFromEvent())); + this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.refreshFromEvent())); this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this.refreshFromEvent())); this.disposables.push(this.labelService.onDidRegisterFormatter(() => { this._onDidChangeTitleArea.fire(); @@ -207,20 +211,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView })); } - layoutBody(size: number): void { - if (this.treeContainer) { - this.treeContainer.style.height = size + 'px'; - } - super.layoutBody(size); - } - - public getActions(): IAction[] { + getActions(): IAction[] { const actions: Action[] = []; - actions.push(this.instantiationService.createInstance(NewFileAction, this.getViewer(), null)); - actions.push(this.instantiationService.createInstance(NewFolderAction, this.getViewer(), null)); + actions.push(this.instantiationService.createInstance(NewFileAction, this.tree, this._editableExplorerItems, null)); + actions.push(this.instantiationService.createInstance(NewFolderAction, this.tree, this._editableExplorerItems, null)); actions.push(this.instantiationService.createInstance(RefreshViewExplorerAction, this, 'explorer-action refresh-explorer')); - actions.push(this.instantiationService.createInstance(CollapseAction, this.getViewer(), true, 'explorer-action collapse-explorer')); + actions.push(this.instantiationService.createInstance(CollapseAction2, this.tree, true, 'explorer-action collapse-explorer')); return actions; } @@ -237,9 +234,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const activeFile = this.getActiveFile(); if (activeFile) { - // Always remember last opened file - this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = activeFile.toString(); - // Select file if input is inside workspace if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { const selection = this.hasSingleSelection(activeFile); @@ -254,17 +248,16 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView // Handle closed or untitled file (convince explorer to not reopen any file when getting visible) const activeInput = this.editorService.activeEditor; if (!activeInput || toResource(activeInput, { supportSideBySide: true, filter: Schemas.untitled })) { - this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = void 0; clearFocus = true; } // Otherwise clear if (clearSelection) { - this.explorerViewer.clearSelection(); + this.tree.setSelection([]); } if (clearFocus) { - this.explorerViewer.clearFocus(); + this.tree.setFocus([]); } } @@ -298,22 +291,22 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } } - public focus(): void { + focus(): void { super.focus(); let keepFocus = false; // Make sure the current selected element is revealed - if (this.explorerViewer) { + if (this.tree) { if (this.autoReveal) { - const selection = this.explorerViewer.getSelection(); + const selection = this.tree.getSelection(); if (selection.length > 0) { this.reveal(selection[0], 0.5); } } // Pass Focus to Viewer - this.explorerViewer.domFocus(); + this.tree.domFocus(); keepFocus = true; } @@ -324,12 +317,12 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } } - public setVisible(visible: boolean): void { + setVisible(visible: boolean): void { super.setVisible(visible); // Show if (visible) { - + DOM.show(this.tree.getHTMLElement()); // If a refresh was requested and we are now visible, run it let refreshPromise: Thenable = Promise.resolve(null); if (this.shouldRefresh) { @@ -357,13 +350,9 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } // Otherwise restore last used file: By lastActiveFileResource - let lastActiveFileResource: URI; - if (this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]) { - lastActiveFileResource = URI.parse(this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]); - } - - if (lastActiveFileResource && this.isCreated && this.model.findClosest(lastActiveFileResource)) { - this.editorService.openEditor({ resource: lastActiveFileResource, options: { revealIfVisible: true } }); + const focusedElements = this.tree.getFocus(); + if (focusedElements && focusedElements.length) { + this.editorService.openEditor({ resource: focusedElements[0].resource, options: { revealIfVisible: true } }); return; } @@ -371,11 +360,19 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView refreshPromise.then(() => { this.openFocusedElement(); }); + } else { + // make sure the tree goes out of the tabindex world by hiding it + DOM.hide(this.tree.getHTMLElement()); } } + collapseAll(): void { + this.tree.collapseAll(); + } + private openFocusedElement(preserveFocus?: boolean): void { - const stat: ExplorerItem = this.explorerViewer.getFocus(); + const focusedElements = this.tree.getFocus(); + const stat = focusedElements && focusedElements.length ? focusedElements[0] : undefined; if (stat && !stat.isDirectory) { this.editorService.openEditor({ resource: stat.resource, options: { preserveFocus, revealIfVisible: true } }); } @@ -393,10 +390,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return toResource(input, { supportSideBySide: true }); } - private get isCreated(): boolean { - return !!(this.explorerViewer && this.explorerViewer.getInput()); - } - @memoize private get model(): Model { const model = this.instantiationService.createInstance(Model); @@ -405,73 +398,69 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return model; } - private createViewer(container: HTMLElement): WorkbenchTree { - const dataSource = this.instantiationService.createInstance(FileDataSource); - const renderer = this.instantiationService.createInstance(FileRenderer, this.fileViewletState); - const controller = this.instantiationService.createInstance(FileController); - this.disposables.push(controller); - const sorter = this.instantiationService.createInstance(FileSorter); - this.disposables.push(sorter); - this.filter = this.instantiationService.createInstance(FileFilter); + private createTree(container: HTMLElement): void { + // TODO@isidor missing features + // const controller = this.instantiationService.createInstance(FileController); + // this.disposables.push(controller); + // const sorter = this.instantiationService.createInstance(FileSorter); + // this.disposables.push(sorter); + // const dnd = this.instantiationService.createInstance(FileDragAndDrop); + this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); - const dnd = this.instantiationService.createInstance(FileDragAndDrop); - const accessibilityProvider = this.instantiationService.createInstance(FileAccessibilityProvider); + const filesRenderer = this.instantiationService.createInstance(FilesRenderer, this._editableExplorerItems); + this.disposables.push(filesRenderer); - this.explorerViewer = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, container, { - dataSource, - renderer, - controller, - sorter, - filter: this.filter, - dnd, - accessibilityProvider - }, { - autoExpandSingleChildren: true, - ariaLabel: nls.localize('treeAriaLabel', "Files Explorer") - }); + this.tree = new WorkbenchAsyncDataTree(container, new ExplorerDelegate(), [filesRenderer], + this.instantiationService.createInstance(ExplorerDataSource, this.model), { + accessibilityProvider: new ExplorerAccessibilityProvider(), + ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), + identityProvider: { + getId: stat => stat.resource + }, + typeLabelProvider: { + getTypeLabel: stat => stat.name + }, + filter: this.filter + }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); + filesRenderer.acquireTree(this.tree); // Bind context keys - FilesExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); - ExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); + FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); + ExplorerFocusedContext.bindTo(this.tree.contextKeyService); // Update Viewer based on File Change Events this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); // Update resource context based on focused element - this.disposables.push(this.explorerViewer.onDidChangeFocus((e: { focus: ExplorerItem }) => { + this.disposables.push(this.tree.onDidChangeFocus(e => { + const stat = e.elements && e.elements.length ? e.elements[0] : undefined; const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER; - const resource = e.focus ? e.focus.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : undefined; + const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : undefined; this.resourceContext.set(resource); - this.folderContext.set((isSingleFolder && !e.focus) || e.focus && e.focus.isDirectory); - this.readonlyContext.set(e.focus && e.focus.isReadonly); - this.rootContext.set(!e.focus || (e.focus && e.focus.isRoot)); + this.folderContext.set((isSingleFolder && !stat) || stat && stat.isDirectory); + this.readonlyContext.set(stat && stat.isReadonly); + this.rootContext.set(!stat || (stat && stat.isRoot)); })); // Open when selecting via keyboard - this.disposables.push(this.explorerViewer.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - const element = this.tree.getSelection(); + this.disposables.push(this.tree.onDidChangeSelection(e => { + const selection = e.elements; + if (selection && selection.length) { + // TODO@Isidor check this, and add side by side, focus passing + const stat = selection[0]; - if (Array.isArray(element) && element[0] instanceof ExplorerItem) { - if (element[0].isDirectory) { - this.explorerViewer.toggleExpansion(element[0]); - } - - controller.openEditor(element[0], { pinned: false, sideBySide: false, preserveFocus: false }); + if (!stat.isDirectory) { + this.editorService.openEditor({ resource: stat.resource }); } } })); - return this.explorerViewer; + this.disposables.push(this.tree.onContextMenu(e => this.onContextMenu(e))); } - getViewer(): WorkbenchTree { - return this.tree; - } - - public getOptimalWidth(): number { - const parentNode = this.explorerViewer.getHTMLElement(); + getOptimalWidth(): number { + const parentNode = this.tree.getHTMLElement(); const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels return DOM.getLargestChildWidth(parentNode, childNodes); @@ -501,15 +490,14 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } const childElement = ExplorerItem.create(addedElement, p.root); - p.removeChild(childElement); // make sure to remove any previous version of the file if any + // Make sure to remove any previous version of the file if any + p.removeChild(childElement); 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); - }); + this.tree.refresh(p).then(() => { + // Reveal and focus new element + this.reveal(childElement, 0.5); + this.tree.setFocus([childElement]); }); }); }); @@ -526,7 +514,8 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView // Only update focus if renamed/moved element is selected let restoreFocus = false; - const focus: ExplorerItem = this.explorerViewer.getFocus(); + const focusedElements = this.tree.getFocus(); + const focus = focusedElements && focusedElements.length ? focusedElements[0] : undefined; if (focus && focus.resource && focus.resource.toString() === oldResource.toString()) { restoreFocus = true; } @@ -537,20 +526,20 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const modelElements = this.model.findAll(oldResource); modelElements.forEach(modelElement => { //Check if element is expanded - isExpanded = this.explorerViewer.isExpanded(modelElement); + isExpanded = this.tree.isExpanded(modelElement); // Rename File (Model) modelElement.rename(newElement); // Update Parent (View) - this.explorerViewer.refresh(modelElement.parent).then(() => { + this.tree.refresh(modelElement.parent).then(() => { // Select in Viewer if set if (restoreFocus) { - this.explorerViewer.setFocus(modelElement); + this.tree.setFocus([modelElement]); } //Expand the element again if (isExpanded) { - this.explorerViewer.expand(modelElement); + this.tree.expand(modelElement); } }); }); @@ -568,10 +557,10 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const oldParent = modelElement.parent; modelElement.move(newParents[index], (callback: () => void) => { // Update old parent - this.explorerViewer.refresh(oldParent).then(callback); + this.tree.refresh(oldParent).then(callback); }, () => { // Update new parent - this.explorerViewer.refresh(newParents[index], true).then(() => this.explorerViewer.expand(newParents[index])); + this.tree.refresh(newParents[index], true).then(() => this.tree.expand(newParents[index])); }); }); } @@ -588,12 +577,12 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView parent.removeChild(element); // Refresh Parent (View) - const restoreFocus = this.explorerViewer.isDOMFocused(); - this.explorerViewer.refresh(parent).then(() => { + const restoreFocus = document.activeElement === this.tree.getHTMLElement(); + this.tree.refresh(parent).then(() => { // Ensure viewer has keyboard focus if event originates from viewer if (restoreFocus) { - this.explorerViewer.domFocus(); + this.tree.domFocus(); } }); } @@ -602,17 +591,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } private onFileChanges(e: FileChangesEvent): void { - - // Ensure memento state does not capture a deleted file (we run this from a timeout because - // delete events can result in UI activity that will fill the memento again when multiple - // editors are closing) - setTimeout(() => { - const lastActiveResource: string = this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]; - if (lastActiveResource && e.contains(URI.parse(lastActiveResource), FileChangeType.DELETED)) { - this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = null; - } - }); - // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might // be fired first over the other or not at all. @@ -713,17 +691,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void { if (this.isVisible() && !this.isDisposed) { this.explorerRefreshDelayer.trigger(() => { - if (!this.explorerViewer.getHighlight()) { - return this.doRefresh(newRoots.map(r => r.uri)).then(() => { - if (newRoots.length === 1) { - return this.reveal(this.model.findClosest(newRoots[0].uri), 0.5); - } + return this.doRefresh().then(() => { + if (newRoots.length === 1) { + return this.reveal(this.model.findClosest(newRoots[0].uri), 0.5); + } - return undefined; - }); - } - - return Promise.resolve(null); + return undefined; + }); }); } else { this.shouldRefresh = true; @@ -733,22 +707,22 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView /** * Refresh the contents of the explorer to get up to date data from the disk about the file structure. */ - public refresh(): TPromise { - if (!this.explorerViewer || this.explorerViewer.getHighlight()) { + refresh(): TPromise { + if (!this.tree) { return Promise.resolve(null); } // Focus - this.explorerViewer.domFocus(); + this.tree.domFocus(); // Find resource to focus from active editor input if set let resourceToFocus: URI; if (this.autoReveal) { resourceToFocus = this.getActiveFile(); if (!resourceToFocus) { - const selection = this.explorerViewer.getSelection(); + const selection = this.tree.getSelection(); if (selection && selection.length === 1) { - resourceToFocus = (selection[0]).resource; + resourceToFocus = selection[0].resource; } } } @@ -762,7 +736,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }); } - private doRefresh(targetsToExpand: URI[] = []): TPromise { + private doRefresh(): TPromise { const targetsToResolve = this.model.roots.map(root => ({ root, resource: root.resource, options: { resolveTo: [] } })); // First time refresh: Receive target through active editor input or selection and also include settings from previous session @@ -775,14 +749,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView found.options.resolveTo.push(activeFile); } } - - targetsToExpand.forEach(toExpand => { - const workspaceFolder = this.contextService.getWorkspaceFolder(toExpand); - if (workspaceFolder) { - const found = targetsToResolve.filter(ttr => ttr.resource.toString() === workspaceFolder.uri.toString()).pop(); - found.options.resolveTo.push(toExpand); - } - }); } // Subsequent refresh: Receive targets through expanded folders in tree @@ -792,7 +758,8 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }); } - const promise = this.resolveRoots(targetsToResolve, targetsToExpand).then(result => { + const promise = this.resolveRoots(targetsToResolve).then(result => { + this.isCreated = true; this.decorationProvider.changed(targetsToResolve.map(t => t.root.resource)); return result; }); @@ -801,19 +768,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return promise; } - private resolveRoots(targetsToResolve: { root: ExplorerItem, resource: URI, options: { resolveTo: any[] } }[], targetsToExpand: URI[]): TPromise { + private resolveRoots(targetsToResolve: { root: ExplorerItem, resource: URI, options: { resolveTo: any[] } }[]): TPromise { - // Display roots only when multi folder workspace - let input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model; - if (input !== this.explorerViewer.getInput()) { + if (!this.isCreated) { perf.mark('willResolveExplorer'); } const errorRoot = (resource: URI, root: ExplorerItem) => { - if (input === this.model.roots[0]) { - input = this.model; - } - return ExplorerItem.create({ resource: resource, name: paths.basename(resource.fsPath), @@ -823,17 +784,6 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }, root, undefined, true); }; - const setInputAndExpand = (input: ExplorerItem | Model, statsToExpand: ExplorerItem[]) => { - // Make sure to expand all folders that where expanded in the previous session - // Special case: we are switching to multi workspace view, thus expand all the roots (they might just be added) - if (input === this.model && statsToExpand.every(fs => fs && !fs.isRoot)) { - statsToExpand = this.model.roots.concat(statsToExpand); - } - - return this.explorerViewer.setInput(input).then(() => this.explorerViewer.expandAll(statsToExpand)) - .then(() => perf.mark('didResolveExplorer')); - }; - if (targetsToResolve.every(t => t.root.resource.scheme === 'file')) { // All the roots are local, resolve them in parallel return this.fileService.resolveFiles(targetsToResolve).then(results => { @@ -852,19 +802,11 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } }); - const statsToExpand: ExplorerItem[] = this.explorerViewer.getExpandedElements().concat(targetsToExpand.map(expand => this.model.findClosest(expand))); - if (input === this.explorerViewer.getInput()) { - return this.explorerViewer.refresh().then(() => this.explorerViewer.expandAll(statsToExpand)); - } - - return setInputAndExpand(input, statsToExpand); + return this.tree.refresh(null); }); } // There is a remote root, resolve the roots sequantally - let statsToExpand: ExplorerItem[] = []; - let delayer = new Delayer(100); - let delayerPromise: TPromise; return Promise.all(targetsToResolve.map((target, index) => this.fileService.resolveFile(target.resource, target.options) .then(result => result.isDirectory ? ExplorerItem.create(result, target.root, target.options.resolveTo) : errorRoot(target.resource, target.root), () => errorRoot(target.resource, target.root)) .then(modelStat => { @@ -873,20 +815,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView ExplorerItem.mergeLocalWithDisk(modelStat, this.model.roots[index]); } - let toExpand: ExplorerItem[] = this.explorerViewer.getExpandedElements().concat(targetsToExpand.map(target => this.model.findClosest(target))); - if (input === this.explorerViewer.getInput()) { - statsToExpand = statsToExpand.concat(toExpand); - if (!delayer.isTriggered()) { - delayerPromise = delayer.trigger(() => this.explorerViewer.refresh() - .then(() => this.explorerViewer.expandAll(statsToExpand)) - .then(() => statsToExpand = []) - ); - } - - return delayerPromise; - } - - return setInputAndExpand(input, statsToExpand); + return this.tree.refresh(null); }))); } @@ -920,34 +849,39 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to * resolve the path from the disk in case the explorer is not yet expanded to the file yet. */ - public select(resource: URI, reveal: boolean = this.autoReveal): TPromise { + select(resource: URI, reveal: boolean = this.autoReveal): void { // Require valid path if (!resource) { - return Promise.resolve(null); + return; } // If path already selected, just reveal and return const selection = this.hasSingleSelection(resource); if (selection) { - return reveal ? this.reveal(selection, 0.5) : Promise.resolve(null); + if (reveal) { + this.reveal(selection, 0.5); + } + + return; } // First try to get the stat object from the input to avoid a roundtrip if (!this.isCreated) { - return Promise.resolve(null); + return; } const fileStat = this.model.findClosest(resource); if (fileStat) { - return this.doSelect(fileStat, reveal); + this.doSelect(fileStat, reveal); + return; } // Stat needs to be resolved first and then revealed const options: IResolveFileOptions = { resolveTo: [resource] }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); const rootUri = workspaceFolder ? workspaceFolder.uri : this.model.roots[0].resource; - return this.fileService.resolveFile(rootUri, options).then(stat => { + this.fileService.resolveFile(rootUri, options).then(stat => { // Convert to model const root = this.model.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); @@ -956,77 +890,85 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView ExplorerItem.mergeLocalWithDisk(modelStat, root); // Select and Reveal - return this.explorerViewer.refresh(root).then(() => this.doSelect(root.find(resource), reveal)); + return this.tree.refresh(root).then(() => this.doSelect(root.find(resource), reveal)); }, e => { this.notificationService.error(e); }); } private hasSingleSelection(resource: URI): ExplorerItem { - const currentSelection: ExplorerItem[] = this.explorerViewer.getSelection(); + const currentSelection: ExplorerItem[] = this.tree.getSelection(); return currentSelection.length === 1 && currentSelection[0].resource.toString() === resource.toString() ? currentSelection[0] : undefined; } - private doSelect(fileStat: ExplorerItem, reveal: boolean): TPromise { + private doSelect(fileStat: ExplorerItem, reveal: boolean): void { if (!fileStat) { - return Promise.resolve(null); + return; } - // Special case: we are asked to reveal and select an element that is not visible - // In this case we take the parent element so that we are at least close to it. - if (!this.filter.isVisible(this.tree, fileStat)) { - fileStat = fileStat.parent; - if (!fileStat) { - return Promise.resolve(null); - } - } + // // Special case: we are asked to reveal and select an element that is not visible + // // In this case we take the parent element so that we are at least close to it. + // if (!this.tree.isVisible(fileStat)) { + // fileStat = fileStat.parent; + // if (!fileStat) { + // return; + // } + // } - // Reveal depending on flag - let revealPromise: TPromise; if (reveal) { - revealPromise = this.reveal(fileStat, 0.5); - } else { - revealPromise = Promise.resolve(null); + this.reveal(fileStat, 0.5); } - return revealPromise.then(() => { - if (!fileStat.isDirectory) { - this.explorerViewer.setSelection([fileStat]); // Since folders can not be opened, only select files - } + if (!fileStat.isDirectory) { + this.tree.setSelection([fileStat]); // Since folders can not be opened, only select files + } - this.explorerViewer.setFocus(fileStat); + this.tree.setFocus([fileStat]); + } + + private reveal(element: any, relativeTop?: number): void { + if (this.tree) { + this.tree.reveal(element, relativeTop); + } + } + + private onContextMenu(e: ITreeContextMenuEvent): void { + const stat = e.element; + if (stat instanceof NewStatPlaceholder) { + return; + } + + // update dynamic contexts + this.fileCopiedContextKey.set(this.clipboardService.hasResources()); + + const selection = this.tree.getSelection(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => { + const actions: IAction[] = []; + fillInContextMenuActions(this.contributedContextMenu, { arg: stat instanceof ExplorerItem ? stat.resource : {}, shouldForwardArgs: true }, actions, this.contextMenuService); + return actions; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree.domFocus(); + } + }, + getActionsContext: () => selection && selection.indexOf(stat) >= 0 + ? selection.map((fs: ExplorerItem) => fs.resource) + : stat instanceof ExplorerItem ? [stat.resource] : [] }); } - private reveal(element: any, relativeTop?: number): TPromise { - if (!this.tree) { - return Promise.resolve(null); // return early if viewlet has not yet been created - } - return this.tree.reveal(element, relativeTop); + @memoize private get contributedContextMenu(): IMenu { + const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService); + this.disposables.push(contributedContextMenu); + return contributedContextMenu; } - saveState(): void { - - // Keep list of expanded folders to restore on next load - if (this.isCreated) { - const expanded = this.explorerViewer.getExpandedElements() - .filter(e => e instanceof ExplorerItem) - .map((e: ExplorerItem) => e.resource.toString()); - - if (expanded.length) { - this.viewState[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES] = expanded; - } else { - delete this.viewState[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]; - } - } - - // Clean up last focused if not set - if (!this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]) { - delete this.viewState[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]; - } - - super.saveState(); + @memoize private get fileCopiedContextKey(): IContextKey { + return FileCopiedContext.bindTo(this.contextKeyService); } dispose(): void { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index c54fdec00f8..3d41d7fbd57 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -3,112 +3,103 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TPromise } from 'vs/base/common/winjs.base'; -import * as nls from 'vs/nls'; -import * as objects from 'vs/base/common/objects'; +import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; -import * as path from 'path'; -import { URI } from 'vs/base/common/uri'; -import { once } from 'vs/base/common/functional'; -import * as paths from 'vs/base/common/paths'; -import * as resources from 'vs/base/common/resources'; -import * as errors from 'vs/base/common/errors'; -import { IAction, ActionRunner as BaseActionRunner, IActionRunner } from 'vs/base/common/actions'; -import * as comparers from 'vs/base/common/comparers'; -import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { isMacintosh, isLinux } from 'vs/base/common/platform'; import * as glob from 'vs/base/common/glob'; -import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; -import { DuplicateFileAction, AddFilesAction, IEditableData, IFileViewletState, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; -import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, 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 } from 'vs/base/parts/tree/browser/treeDefaults'; -import { ExplorerItem, NewStatPlaceholder, Model } from 'vs/workbench/parts/files/common/explorerModel'; -import { DragMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IFileService, FileKind } from 'vs/platform/files/common/files'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProgressService } from 'vs/platform/progress/common/progress'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; +import { IFileViewletState, IEditableData } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowService } from 'vs/platform/windows/common/windows'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; -import { extractResources, SimpleFileResourceDragAndDrop, CodeDataTransfers, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; -import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { DataTransfers } from 'vs/base/browser/dnd'; -import { Schemas } from 'vs/base/common/network'; -import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFilesConfiguration } from 'vs/workbench/parts/files/common/files'; +import { dirname, joinPath, basename } from 'vs/base/common/resources'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { localize } from 'vs/nls'; +import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { once } from 'vs/base/common/functional'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { normalize, join, nativeSep } from 'vs/base/common/paths'; import { rtrim } from 'vs/base/common/strings'; -import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { equals, deepClone } from 'vs/base/common/objects'; +import * as path from 'path'; + +export class ExplorerDelegate implements IListVirtualDelegate { + + private static readonly ITEM_HEIGHT = 22; + + getHeight(element: ExplorerItem): number { + return ExplorerDelegate.ITEM_HEIGHT; + } + + getTemplateId(element: ExplorerItem): string { + return FilesRenderer.ID; + } +} + +export class ExplorerDataSource implements IDataSource { -export class FileDataSource implements IDataSource { constructor( + private model: Model, @IProgressService private progressService: IProgressService, @INotificationService private notificationService: INotificationService, @IFileService private fileService: IFileService, - @IPartService private partService: IPartService + @IPartService private partService: IPartService, + @IWorkspaceContextService private contextService: IWorkspaceContextService ) { } - public getId(tree: ITree, stat: ExplorerItem | Model): string { - if (stat instanceof Model) { - return 'model'; - } - - return `${stat.root.resource.toString()}:${stat.getId()}`; + hasChildren(element: ExplorerItem | null): boolean { + return element === null || element.isDirectory; } - public hasChildren(tree: ITree, stat: ExplorerItem | Model): boolean { - return stat instanceof Model || (stat instanceof ExplorerItem && (stat.isDirectory || stat.isRoot)); - } - - public getChildren(tree: ITree, stat: ExplorerItem | Model): TPromise { - if (stat instanceof Model) { - return Promise.resolve(stat.roots); + getChildren(element: ExplorerItem | null): Thenable { + if (element === null) { + if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || this.model.roots[0].isError) { + // Display roots only when multi folder workspace + return Promise.resolve(this.model.roots); + } + element = this.model.roots[0]; } // Return early if stat is already resolved - if (stat.isDirectoryResolved) { - return Promise.resolve(stat.getChildrenArray()); + if (element.isDirectoryResolved) { + return Promise.resolve(element.getChildrenArray()); } // Resolve children and add to fileStat for future lookup else { - // Resolve - const promise = this.fileService.resolveFile(stat.resource, { resolveSingleChildDescendants: true }).then(dirStat => { + const promise = this.fileService.resolveFile(element.resource, { resolveSingleChildDescendants: true }).then(dirStat => { // Convert to view model - const modelDirStat = ExplorerItem.create(dirStat, stat.root); + const modelDirStat = ExplorerItem.create(dirStat, element.root); // Add children to folder const children = modelDirStat.getChildrenArray(); if (children) { children.forEach(child => { - stat.addChild(child); + element.addChild(child); }); } - stat.isDirectoryResolved = true; + element.isDirectoryResolved = true; - return stat.getChildrenArray(); + return element.getChildrenArray(); }, (e: any) => { // Do not show error for roots since we already use an explorer decoration to notify user - if (!(stat instanceof ExplorerItem && stat.isRoot)) { + if (!(element instanceof ExplorerItem && element.isRoot)) { this.notificationService.error(e); } @@ -120,30 +111,15 @@ export class FileDataSource implements IDataSource { return promise; } } - - public getParent(tree: ITree, stat: ExplorerItem | Model): TPromise { - if (!stat) { - return Promise.resolve(null); // can be null if nothing selected in the tree - } - - // Return if root reached - if (tree.getInput() === stat) { - return Promise.resolve(null); - } - - // Return if parent already resolved - if (stat instanceof ExplorerItem && stat.parent) { - return Promise.resolve(stat.parent); - } - - // We never actually resolve the parent from the disk for performance reasons. It wouldnt make - // any sense to resolve parent by parent with requests to walk up the chain. Instead, the explorer - // makes sure to properly resolve a deep path to a specific file and merges the result with the model. - return Promise.resolve(null); - } } -export class FileViewletState implements IFileViewletState { +export interface IFileTemplateData { + elementDisposable: IDisposable; + label: FileLabel; + container: HTMLElement; +} + +export class EditableExplorerItems implements IFileViewletState { private editableStats: Map; constructor() { @@ -165,38 +141,16 @@ export class FileViewletState implements IFileViewletState { } } -export class ActionRunner extends BaseActionRunner implements IActionRunner { - private viewletState: FileViewletState; +export class FilesRenderer implements ITreeRenderer, IDisposable { + static readonly ID = 'file'; - constructor(state: FileViewletState) { - super(); - - this.viewletState = state; - } - - public run(action: IAction, context?: any): TPromise { - return super.run(action, { viewletState: this.viewletState }); - } -} - -export interface IFileTemplateData { - elementDisposable: IDisposable; - label: FileLabel; - container: HTMLElement; -} - -// Explorer Renderer -export class FileRenderer implements IRenderer { - - private static readonly ITEM_HEIGHT = 22; - private static readonly FILE_TEMPLATE_ID = 'file'; - - private state: FileViewletState; + private state: EditableExplorerItems; private config: IFilesConfiguration; private configListener: IDisposable; + private tree: WorkbenchAsyncDataTree; constructor( - state: FileViewletState, + state: EditableExplorerItems, @IContextViewService private contextViewService: IContextViewService, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, @@ -213,33 +167,24 @@ export class FileRenderer implements IRenderer { }); } - dispose(): void { - this.configListener.dispose(); + get templateId(): string { + return FilesRenderer.ID; } - public getHeight(tree: ITree, element: any): number { - return FileRenderer.ITEM_HEIGHT; + acquireTree(tree: WorkbenchAsyncDataTree): void { + this.tree = tree; } - public getTemplateId(tree: ITree, element: any): string { - return FileRenderer.FILE_TEMPLATE_ID; - } - - public disposeTemplate(tree: ITree, templateId: string, templateData: IFileTemplateData): void { - templateData.elementDisposable.dispose(); - templateData.label.dispose(); - } - - public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData { + renderTemplate(container: HTMLElement): IFileTemplateData { const elementDisposable = Disposable.None; const label = this.instantiationService.createInstance(FileLabel, container, void 0); return { elementDisposable, label, container }; } - public renderElement(tree: ITree, stat: ExplorerItem, templateId: string, templateData: IFileTemplateData): void { + renderElement(element: ITreeNode, index: number, templateData: IFileTemplateData): void { templateData.elementDisposable.dispose(); - + const stat = element.element; const editableData: IEditableData = this.state.getEditableData(stat); // File Label @@ -254,19 +199,20 @@ export class FileRenderer implements IRenderer { }); templateData.elementDisposable = templateData.label.onDidRender(() => { - tree.updateWidth(stat); + // todo@isidor horizontal scrolling + // this.tree.updateWidth(stat); }); } // Input Box else { templateData.label.element.style.display = 'none'; - this.renderInputBox(templateData.container, tree, stat, editableData); + this.renderInputBox(templateData.container, stat, editableData); templateData.elementDisposable = Disposable.None; } } - private renderInputBox(container: HTMLElement, tree: ITree, stat: ExplorerItem, editableData: IEditableData): void { + private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): void { // Use a file label only for the icon next to the input box const label = this.instantiationService.createInstance(FileLabel, container, void 0); @@ -274,22 +220,22 @@ export class FileRenderer implements IRenderer { const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : (stat.isDirectory || (stat instanceof NewStatPlaceholder && stat.isDirectoryPlaceholder())) ? FileKind.FOLDER : FileKind.FILE; const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses }; - const parent = stat.name ? resources.dirname(stat.resource) : stat.resource; + const parent = stat.name ? dirname(stat.resource) : stat.resource; const value = stat.name || ''; - label.setFile(resources.joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty. + label.setFile(joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty. // Input field for name const inputBox = new InputBox(label.element, this.contextViewService, { validationOptions: { validation: editableData.validator }, - ariaLabel: nls.localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.") + ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.") }); const styler = attachInputBoxStyler(inputBox, this.themeService); inputBox.onDidChange(value => { - label.setFile(resources.joinPath(parent, value || ' '), labelOptions); // update label icon while typing! + label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing! }); const lastDot = value.lastIndexOf('.'); @@ -299,20 +245,17 @@ export class FileRenderer implements IRenderer { inputBox.focus(); const done = once((commit: boolean, blur: boolean) => { - tree.clearHighlight(); label.element.style.display = 'none'; + if (stat instanceof NewStatPlaceholder) { + stat.destroy(); + } if (commit && inputBox.value) { editableData.action.run({ value: inputBox.value }); } - - setTimeout(() => { - if (!blur) { // https://github.com/Microsoft/vscode/issues/20269 - tree.domFocus(); - } - dispose(toDispose); - container.removeChild(label.element); - }, 0); + dispose(toDispose); + container.removeChild(label.element); + this.tree.refresh(stat.parent).then(() => this.tree.domFocus()); }); const toDispose = [ @@ -330,7 +273,7 @@ export class FileRenderer implements IRenderer { const initialRelPath: string = path.relative(stat.root.resource.path, stat.parent.resource.path); let projectFolderName: string = ''; if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - projectFolderName = paths.basename(stat.root.resource.path); // show root folder name in multi-folder project + projectFolderName = basename(stat.root.resource); // show root folder name in multi-folder project } this.showInputMessage(inputBox, initialRelPath, projectFolderName, editableData.action.id); }), @@ -346,24 +289,24 @@ export class FileRenderer implements IRenderer { if (inputBox.validate()) { const value = inputBox.value; if (value && /.[\\/]./.test(value)) { // only show if there's at least one slash enclosed in the string - let displayPath = path.normalize(path.join(projectFolderName, initialRelPath, value)); - displayPath = rtrim(displayPath, paths.nativeSep); + let displayPath = normalize(join(projectFolderName, initialRelPath, value)); + displayPath = rtrim(displayPath, nativeSep); - const indexLastSlash: number = displayPath.lastIndexOf(paths.nativeSep); + const indexLastSlash: number = displayPath.lastIndexOf(nativeSep); const name: string = displayPath.substring(indexLastSlash + 1); const leadingPathPart: string = displayPath.substring(0, indexLastSlash); let msg: string; switch (actionID) { case 'workbench.files.action.createFileFromExplorer': - msg = nls.localize('createFileFromExplorerInfoMessage', "Create file **{0}** in **{1}**", name, leadingPathPart); + msg = localize('createFileFromExplorerInfoMessage', "Create file **{0}** in **{1}**", name, leadingPathPart); break; case 'workbench.files.action.renameFile': - msg = nls.localize('renameFileFromExplorerInfoMessage', "Move and rename to **{0}**", displayPath); + msg = localize('renameFileFromExplorerInfoMessage', "Move and rename to **{0}**", displayPath); break; case 'workbench.files.action.createFolderFromExplorer': // fallthrough default: - msg = nls.localize('createFolderFromExplorerInfoMessage', "Create folder **{0}** in **{1}**", name, leadingPathPart); + msg = localize('createFolderFromExplorerInfoMessage', "Create folder **{0}** in **{1}**", name, leadingPathPart); } inputBox.showMessage({ @@ -373,7 +316,7 @@ export class FileRenderer implements IRenderer { }); } else if (value && /^\s|\s$/.test(value)) { inputBox.showMessage({ - content: nls.localize('whitespace', "Leading or trailing whitespace detected"), + content: localize('whitespace', "Leading or trailing whitespace detected"), formatContent: true, type: MessageType.WARNING }); @@ -382,322 +325,33 @@ export class FileRenderer implements IRenderer { } } } -} -// Explorer Accessibility Provider -export class FileAccessibilityProvider implements IAccessibilityProvider { + disposeElement?(element: ITreeNode, index: number, templateData: IFileTemplateData): void { + // noop + } - public getAriaLabel(tree: ITree, stat: ExplorerItem): string { - return stat.name; + disposeTemplate(templateData: IFileTemplateData): void { + templateData.elementDisposable.dispose(); + templateData.label.dispose(); + } + + dispose(): void { + this.configListener.dispose(); } } -// Explorer Controller -export class FileController extends WorkbenchTreeController implements IDisposable { - private fileCopiedContextKey: IContextKey; - private contributedContextMenu: IMenu; - private toDispose: IDisposable[]; - private previousSelectionRangeStop: ExplorerItem; - - constructor( - @IEditorService private editorService: IEditorService, - @IContextMenuService private contextMenuService: IContextMenuService, - @ITelemetryService private telemetryService: ITelemetryService, - @IMenuService private menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService, - @IClipboardService private clipboardService: IClipboardService, - @IConfigurationService configurationService: IConfigurationService - ) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); - - this.fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService); - this.toDispose = []; - } - - public onLeftClick(tree: WorkbenchTree, stat: ExplorerItem | Model, event: IMouseEvent, origin: string = 'mouse'): boolean { - const payload = { origin: origin }; - const isDoubleClick = (origin === 'mouse' && event.detail === 2); - - // Handle Highlight Mode - if (tree.getHighlight()) { - - // Cancel Event - event.preventDefault(); - event.stopPropagation(); - - tree.clearHighlight(payload); - - return false; - } - - // Handle root - if (stat instanceof Model) { - tree.clearFocus(payload); - tree.clearSelection(payload); - - return false; - } - - // Cancel Event - const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; - if (!isMouseDown) { - event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise - } - event.stopPropagation(); - - // Set DOM focus - tree.domFocus(); - if (stat instanceof NewStatPlaceholder) { - return true; - } - - // Allow to multiselect - if ((tree.useAltAsMultipleSelectionModifier && event.altKey) || !tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey)) { - const selection = tree.getSelection(); - this.previousSelectionRangeStop = undefined; - if (selection.indexOf(stat) >= 0) { - tree.setSelection(selection.filter(s => s !== stat)); - } else { - tree.setSelection(selection.concat(stat)); - tree.setFocus(stat, payload); - } - } - - // Allow to unselect - else if (event.shiftKey) { - const focus = tree.getFocus(); - if (focus) { - if (this.previousSelectionRangeStop) { - tree.deselectRange(stat, this.previousSelectionRangeStop); - } - tree.selectRange(focus, stat, payload); - this.previousSelectionRangeStop = stat; - } - } - - // Select, Focus and open files - else { - - // Expand / Collapse - if (isDoubleClick || this.openOnSingleClick || this.isClickOnTwistie(event)) { - tree.toggleExpansion(stat, event.altKey); - this.previousSelectionRangeStop = undefined; - } - - const preserveFocus = !isDoubleClick; - tree.setFocus(stat, payload); - - if (isDoubleClick) { - event.preventDefault(); // focus moves to editor, we need to prevent default - } - - tree.setSelection([stat], payload); - - if (!stat.isDirectory && (isDoubleClick || this.openOnSingleClick)) { - let sideBySide = false; - if (event) { - sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; - } - - this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick }); - } - } - - return true; - } - - public onMouseMiddleClick(tree: WorkbenchTree, element: ExplorerItem | Model, event: IMouseEvent): boolean { - let sideBySide = false; - if (event) { - sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; - } - if (element instanceof ExplorerItem && !element.isDirectory) { - this.openEditor(element, { preserveFocus: true, sideBySide, pinned: true }); - } - - return true; - } - - public onContextMenu(tree: WorkbenchTree, stat: ExplorerItem | Model, event: ContextMenuEvent): boolean { - if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') { - return false; - } - - event.preventDefault(); - event.stopPropagation(); - - tree.setFocus(stat); - - // update dynamic contexts - this.fileCopiedContextKey.set(this.clipboardService.hasResources()); - - if (!this.contributedContextMenu) { - this.contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, tree.contextKeyService); - this.toDispose.push(this.contributedContextMenu); - } - - const anchor = { x: event.posx, y: event.posy }; - const selection = tree.getSelection(); - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => { - const actions: IAction[] = []; - fillInContextMenuActions(this.contributedContextMenu, { arg: stat instanceof ExplorerItem ? stat.resource : {}, shouldForwardArgs: true }, actions, this.contextMenuService); - return actions; - }, - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - tree.domFocus(); - } - }, - getActionsContext: () => selection && selection.indexOf(stat) >= 0 - ? selection.map((fs: ExplorerItem) => fs.resource) - : stat instanceof ExplorerItem ? [stat.resource] : [] - }); - - return true; - } - - public openEditor(stat: ExplorerItem, options: { preserveFocus: boolean; sideBySide: boolean; pinned: boolean; }): void { - if (stat && !stat.isDirectory) { - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - - this.editorService.openEditor({ resource: stat.resource, options }, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); - } - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); +export class ExplorerAccessibilityProvider implements IAccessibilityProvider { + getAriaLabel(element: ExplorerItem): string { + return element.name; } } -// Explorer Sorter -export class FileSorter implements ISorter { - private toDispose: IDisposable[]; - private sortOrder: SortOrder; - - constructor( - @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService - ) { - this.toDispose = []; - - this.updateSortOrder(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateSortOrder())); - } - - private updateSortOrder(): void { - this.sortOrder = this.configurationService.getValue('explorer.sortOrder') || 'default'; - } - - public compare(tree: ITree, statA: ExplorerItem, statB: ExplorerItem): number { - - // Do not sort roots - if (statA.isRoot) { - if (statB.isRoot) { - return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index; - } - - return -1; - } - - if (statB.isRoot) { - return 1; - } - - // Sort Directories - switch (this.sortOrder) { - case 'type': - if (statA.isDirectory && !statB.isDirectory) { - return -1; - } - - if (statB.isDirectory && !statA.isDirectory) { - return 1; - } - - if (statA.isDirectory && statB.isDirectory) { - return comparers.compareFileNames(statA.name, statB.name); - } - - break; - - case 'filesFirst': - if (statA.isDirectory && !statB.isDirectory) { - return 1; - } - - if (statB.isDirectory && !statA.isDirectory) { - return -1; - } - - break; - - case 'mixed': - break; // not sorting when "mixed" is on - - default: /* 'default', 'modified' */ - if (statA.isDirectory && !statB.isDirectory) { - return -1; - } - - if (statB.isDirectory && !statA.isDirectory) { - return 1; - } - - break; - } - - // Sort "New File/Folder" placeholders - if (statA instanceof NewStatPlaceholder) { - return -1; - } - - if (statB instanceof NewStatPlaceholder) { - return 1; - } - - // Sort Files - switch (this.sortOrder) { - case 'type': - return comparers.compareFileExtensions(statA.name, statB.name); - - case 'modified': - if (statA.mtime !== statB.mtime) { - return statA.mtime < statB.mtime ? 1 : -1; - } - - return comparers.compareFileNames(statA.name, statB.name); - - default: /* 'default', 'mixed', 'filesFirst' */ - return comparers.compareFileNames(statA.name, statB.name); - } - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } -} - -// Explorer Filter interface CachedParsedExpression { original: glob.IExpression; parsed: glob.ParsedExpression; } -export class FileFilter implements IFilter { - +export class FilesFilter implements ITreeFilter { private hiddenExpressionPerRoot: Map; private workspaceFolderChangeListener: IDisposable; @@ -706,15 +360,10 @@ export class FileFilter implements IFilter { @IConfigurationService private configurationService: IConfigurationService ) { this.hiddenExpressionPerRoot = new Map(); - - this.registerListeners(); - } - - public registerListeners(): void { this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration()); } - public updateConfiguration(): boolean { + updateConfiguration(): boolean { let needsRefresh = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue({ resource: folder.uri }); @@ -722,10 +371,10 @@ export class FileFilter implements IFilter { if (!needsRefresh) { const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); - needsRefresh = !cached || !objects.equals(cached.original, excludesConfig); + needsRefresh = !cached || !equals(cached.original, excludesConfig); } - const excludesConfigCopy = objects.deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods + const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression); }); @@ -733,18 +382,17 @@ export class FileFilter implements IFilter { return needsRefresh; } - public isVisible(tree: ITree, stat: ExplorerItem): boolean { - return this.doIsVisible(stat); - } - - private doIsVisible(stat: ExplorerItem): boolean { + filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult { + if (parentVisibility === TreeVisibility.Hidden) { + return false; + } if (stat instanceof NewStatPlaceholder || stat.isRoot) { return true; // always visible } // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); - if (cached && cached.parsed(paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), stat.name, name => !!stat.parent.getChild(name))) { + if (cached && cached.parsed(normalize(path.relative(stat.root.resource.path, stat.resource.path), true), stat.name, name => !!stat.parent.getChild(name))) { return false; // hidden through pattern } @@ -756,346 +404,597 @@ export class FileFilter implements IFilter { } } -// Explorer Drag And Drop Controller -export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { - - private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; - - private toDispose: IDisposable[]; - private dropEnabled: boolean; - - constructor( - @INotificationService private notificationService: INotificationService, - @IDialogService private dialogService: IDialogService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IFileService private fileService: IFileService, - @IConfigurationService private configurationService: IConfigurationService, - @IInstantiationService instantiationService: IInstantiationService, - @ITextFileService private textFileService: ITextFileService, - @IWindowService private windowService: IWindowService, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService - ) { - super(stat => this.statToResource(stat), instantiationService); - - this.toDispose = []; - - this.updateDropEnablement(); - - this.registerListeners(); - } - - private statToResource(stat: ExplorerItem): URI { - if (stat.isDirectory) { - return URI.from({ scheme: 'folder', path: stat.resource.path }); // indicates that we are dragging a folder - } - - return stat.resource; - } - - private registerListeners(): void { - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateDropEnablement())); - } - - private updateDropEnablement(): void { - this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop'); - } - - public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void { - const sources: ExplorerItem[] = data.getData(); - if (sources && sources.length) { - - // When dragging folders, make sure to collapse them to free up some space - sources.forEach(s => { - if (s.isDirectory && tree.isExpanded(s)) { - tree.collapse(s, false); - } - }); - - // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, sources, originalEvent); - - // The only custom data transfer we set from the explorer is a file transfer - // to be able to DND between multiple code file explorers across windows - const fileResources = sources.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); - if (fileResources.length) { - originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources)); - } - } - } - - public onDragOver(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): IDragOverReaction { - if (!this.dropEnabled) { - return DRAG_OVER_REJECT; - } - - const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); - const fromDesktop = data instanceof DesktopDragAndDropData; - - // Desktop DND - if (fromDesktop) { - const types: string[] = originalEvent.dataTransfer.types; - const typesArray: string[] = []; - for (let i = 0; i < types.length; i++) { - typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase - } - - if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) { - return DRAG_OVER_REJECT; - } - } - - // Other-Tree DND - else if (data instanceof ExternalElementsDragAndDropData) { - return DRAG_OVER_REJECT; - } - - // In-Explorer DND - else { - const sources: ExplorerItem[] = data.getData(); - if (target instanceof Model) { - if (sources[0].isRoot) { - return DRAG_OVER_ACCEPT_BUBBLE_DOWN(false); - } - - return DRAG_OVER_REJECT; - } - - if (!Array.isArray(sources)) { - return DRAG_OVER_REJECT; - } - - if (sources.some((source) => { - if (source instanceof NewStatPlaceholder) { - return true; // NewStatPlaceholders can not be moved - } - - if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) { - return true; // Root folder can not be moved to a non root file stat. - } - - if (source.resource.toString() === target.resource.toString()) { - return true; // Can not move anything onto itself - } - - if (source.isRoot && target instanceof ExplorerItem && target.isRoot) { - // Disable moving workspace roots in one another - return false; - } - - if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) { - return true; // Can not move a file to the same parent unless we copy - } - - if (resources.isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) { - return true; // Can not move a parent folder into one of its children - } - - return false; - })) { - return DRAG_OVER_REJECT; - } - } - - // All (target = model) - if (target instanceof Model) { - return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(false) : DRAG_OVER_REJECT; // can only drop folders to workspace - } - - // All (target = file/folder) - else { - if (target.isDirectory) { - if (target.isReadonly) { - return DRAG_OVER_REJECT; - } - return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true); - } - - if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) { - return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_UP_COPY : DRAG_OVER_ACCEPT_BUBBLE_UP; - } - } - - return DRAG_OVER_REJECT; - } - - public drop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): void { - - // Desktop DND (Import file) - if (data instanceof DesktopDragAndDropData) { - this.handleExternalDrop(tree, data, target, originalEvent); - } - - // In-Explorer DND (Move/Copy file) - else { - this.handleExplorerDrop(tree, data, target, originalEvent); - } - } - - private handleExternalDrop(tree: ITree, data: DesktopDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): TPromise { - const droppedResources = extractResources(originalEvent.browserEvent as DragEvent, true); - - // Check for dropped external files to be folders - return this.fileService.resolveFiles(droppedResources).then(result => { - - // Pass focus to window - this.windowService.focusWindow(); - - // Handle folders by adding to workspace if we are in workspace context - const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource })); - if (folders.length > 0) { - - // If we are in no-workspace context, ask for confirmation to create a workspace - let confirmedPromise: TPromise = Promise.resolve({ confirmed: true }); - if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) { - confirmedPromise = this.dialogService.confirm({ - message: folders.length > 1 ? nls.localize('dropFolders', "Do you want to add the folders to the workspace?") : nls.localize('dropFolder', "Do you want to add the folder to the workspace?"), - type: 'question', - primaryButton: folders.length > 1 ? nls.localize('addFolders', "&&Add Folders") : nls.localize('addFolder', "&&Add Folder") - }); - } - - return confirmedPromise.then(res => { - if (res.confirmed) { - return this.workspaceEditingService.addFolders(folders); - } - - return void 0; - }); - } - - // Handle dropped files (only support FileStat as target) - else if (target instanceof ExplorerItem && !target.isReadonly) { - const addFilesAction = this.instantiationService.createInstance(AddFilesAction, tree, target, null); - - return addFilesAction.run(droppedResources.map(res => res.resource)); - } - - return void 0; - }); - } - - private handleExplorerDrop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): TPromise { - const sources: ExplorerItem[] = resources.distinctParents(data.getData(), s => s.resource); - const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); - - let confirmPromise: TPromise; - - // Handle confirm setting - const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); - if (confirmDragAndDrop) { - confirmPromise = this.dialogService.confirm({ - message: sources.length > 1 && sources.every(s => s.isRoot) ? nls.localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") - : sources.length > 1 ? getConfirmMessage(nls.localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", sources.length), sources.map(s => s.resource)) - : sources[0].isRoot ? nls.localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", sources[0].name) - : nls.localize('confirmMove', "Are you sure you want to move '{0}'?", sources[0].name), - checkbox: { - label: nls.localize('doNotAskAgain', "Do not ask me again") - }, - type: 'question', - primaryButton: nls.localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") - }); - } else { - confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult); - } - - return confirmPromise.then(res => { - - // Check for confirmation checkbox - let updateConfirmSettingsPromise: TPromise = Promise.resolve(void 0); - if (res.confirmed && res.checkboxChecked === true) { - updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER); - } - - return updateConfirmSettingsPromise.then(() => { - if (res.confirmed) { - const rootDropPromise = this.doHandleRootDrop(sources.filter(s => s.isRoot), target); - return Promise.all(sources.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(tree, source, target, isCopy)).concat(rootDropPromise)).then(() => void 0); - } - - return Promise.resolve(void 0); - }); - }); - } - - private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem | Model): TPromise { - if (roots.length === 0) { - return Promise.resolve(undefined); - } - - const folders = this.contextService.getWorkspace().folders; - let targetIndex: number; - const workspaceCreationData: IWorkspaceFolderCreationData[] = []; - const rootsToMove: IWorkspaceFolderCreationData[] = []; - - for (let index = 0; index < folders.length; index++) { - const data = { - uri: folders[index].uri - }; - if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) { - targetIndex = workspaceCreationData.length; - } - - if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) { - workspaceCreationData.push(data); - } else { - rootsToMove.push(data); - } - } - if (target instanceof Model) { - targetIndex = workspaceCreationData.length; - } - - workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); - return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); - } - - private doHandleExplorerDrop(tree: ITree, source: ExplorerItem, target: ExplorerItem | Model, isCopy: boolean): TPromise { - if (!(target instanceof ExplorerItem)) { - return Promise.resolve(void 0); - } - - return tree.expand(target).then(() => { - - if (target.isReadonly) { - return void 0; - } - - // Reuse duplicate action if user copies - if (isCopy) { - return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run(); - } - - // Otherwise move - const targetResource = resources.joinPath(target.resource, source.name); - - return this.textFileService.move(source.resource, targetResource).then(void 0, error => { - - // Conflict - if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - // Move with overwrite if the user confirms - return this.dialogService.confirm(confirm).then(res => { - if (res.confirmed) { - return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(void 0, error => this.notificationService.error(error)); - } - - return void 0; - }); - } - - // Any other error - else { - this.notificationService.error(error); - } - - return void 0; - }); - }, errors.onUnexpectedError); - } -} +// Explorer Controller +// export class FileController extends WorkbenchTreeController implements IDisposable { +// private fileCopiedContextKey: IContextKey; +// private contributedContextMenu: IMenu; +// private previousSelectionRangeStop: ExplorerItem; + +// constructor( +// @IEditorService private editorService: IEditorService, +// @IContextMenuService private contextMenuService: IContextMenuService, +// @ITelemetryService private telemetryService: ITelemetryService, +// @IMenuService private menuService: IMenuService, +// @IContextKeyService contextKeyService: IContextKeyService, +// @IClipboardService private clipboardService: IClipboardService, +// @IConfigurationService configurationService: IConfigurationService +// ) { +// super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); + +// this.fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService); +// } + +// public onLeftClick(tree: WorkbenchTree, stat: ExplorerItem | Model, event: IMouseEvent, origin: string = 'mouse'): boolean { +// const payload = { origin: origin }; +// const isDoubleClick = (origin === 'mouse' && event.detail === 2); + +// // Handle Highlight Mode +// if (tree.getHighlight()) { + +// // Cancel Event +// event.preventDefault(); +// event.stopPropagation(); + +// tree.clearHighlight(payload); + +// return false; +// } + +// // Handle root +// if (stat instanceof Model) { +// tree.clearFocus(payload); +// tree.clearSelection(payload); + +// return false; +// } + +// // Cancel Event +// const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; +// if (!isMouseDown) { +// event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise +// } +// event.stopPropagation(); + +// // Set DOM focus +// tree.domFocus(); +// if (stat instanceof NewStatPlaceholder) { +// return true; +// } + +// // Allow to multiselect +// if ((tree.useAltAsMultipleSelectionModifier && event.altKey) || !tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey)) { +// const selection = tree.getSelection(); +// this.previousSelectionRangeStop = undefined; +// if (selection.indexOf(stat) >= 0) { +// tree.setSelection(selection.filter(s => s !== stat)); +// } else { +// tree.setSelection(selection.concat(stat)); +// tree.setFocus(stat, payload); +// } +// } + +// // Allow to unselect +// else if (event.shiftKey) { +// const focus = tree.getFocus(); +// if (focus) { +// if (this.previousSelectionRangeStop) { +// tree.deselectRange(stat, this.previousSelectionRangeStop); +// } +// tree.selectRange(focus, stat, payload); +// this.previousSelectionRangeStop = stat; +// } +// } + +// // Select, Focus and open files +// else { + +// // Expand / Collapse +// if (isDoubleClick || this.openOnSingleClick || this.isClickOnTwistie(event)) { +// tree.toggleExpansion(stat, event.altKey); +// this.previousSelectionRangeStop = undefined; +// } + +// const preserveFocus = !isDoubleClick; +// tree.setFocus(stat, payload); + +// if (isDoubleClick) { +// event.preventDefault(); // focus moves to editor, we need to prevent default +// } + +// tree.setSelection([stat], payload); + +// if (!stat.isDirectory && (isDoubleClick || this.openOnSingleClick)) { +// let sideBySide = false; +// if (event) { +// sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; +// } + +// this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick }); +// } +// } + +// return true; +// } + +// public onMouseMiddleClick(tree: WorkbenchTree, element: ExplorerItem | Model, event: IMouseEvent): boolean { +// let sideBySide = false; +// if (event) { +// sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; +// } +// if (element instanceof ExplorerItem && !element.isDirectory) { +// this.openEditor(element, { preserveFocus: true, sideBySide, pinned: true }); +// } + +// return true; +// } + +// public openEditor(stat: ExplorerItem, options: { preserveFocus: boolean; sideBySide: boolean; pinned: boolean; }): void { +// if (stat && !stat.isDirectory) { +// /* __GDPR__ +// "workbenchActionExecuted" : { +// "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, +// "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } +// } +// */ +// this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); + +// this.editorService.openEditor({ resource: stat.resource, options }, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); +// } +// } +// } + +// // Explorer Sorter +// export class FileSorter implements ISorter { +// private toDispose: IDisposable[]; +// private sortOrder: SortOrder; + +// constructor( +// @IConfigurationService private configurationService: IConfigurationService, +// @IWorkspaceContextService private contextService: IWorkspaceContextService +// ) { +// this.toDispose = []; + +// this.updateSortOrder(); + +// this.registerListeners(); +// } + +// private registerListeners(): void { +// this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateSortOrder())); +// } + +// private updateSortOrder(): void { +// this.sortOrder = this.configurationService.getValue('explorer.sortOrder') || 'default'; +// } + +// public compare(tree: ITree, statA: ExplorerItem, statB: ExplorerItem): number { + +// // Do not sort roots +// if (statA.isRoot) { +// if (statB.isRoot) { +// return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index; +// } + +// return -1; +// } + +// if (statB.isRoot) { +// return 1; +// } + +// // Sort Directories +// switch (this.sortOrder) { +// case 'type': +// if (statA.isDirectory && !statB.isDirectory) { +// return -1; +// } + +// if (statB.isDirectory && !statA.isDirectory) { +// return 1; +// } + +// if (statA.isDirectory && statB.isDirectory) { +// return comparers.compareFileNames(statA.name, statB.name); +// } + +// break; + +// case 'filesFirst': +// if (statA.isDirectory && !statB.isDirectory) { +// return 1; +// } + +// if (statB.isDirectory && !statA.isDirectory) { +// return -1; +// } + +// break; + +// case 'mixed': +// break; // not sorting when "mixed" is on + +// default: /* 'default', 'modified' */ +// if (statA.isDirectory && !statB.isDirectory) { +// return -1; +// } + +// if (statB.isDirectory && !statA.isDirectory) { +// return 1; +// } + +// break; +// } + +// // Sort "New File/Folder" placeholders +// if (statA instanceof NewStatPlaceholder) { +// return -1; +// } + +// if (statB instanceof NewStatPlaceholder) { +// return 1; +// } + +// // Sort Files +// switch (this.sortOrder) { +// case 'type': +// return comparers.compareFileExtensions(statA.name, statB.name); + +// case 'modified': +// if (statA.mtime !== statB.mtime) { +// return statA.mtime < statB.mtime ? 1 : -1; +// } + +// return comparers.compareFileNames(statA.name, statB.name); + +// default: /* 'default', 'mixed', 'filesFirst' */ +// return comparers.compareFileNames(statA.name, statB.name); +// } +// } + +// public dispose(): void { +// this.toDispose = dispose(this.toDispose); +// } +// } + +// export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { + +// private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; + +// private toDispose: IDisposable[]; +// private dropEnabled: boolean; + +// constructor( +// @INotificationService private notificationService: INotificationService, +// @IDialogService private dialogService: IDialogService, +// @IWorkspaceContextService private contextService: IWorkspaceContextService, +// @IFileService private fileService: IFileService, +// @IConfigurationService private configurationService: IConfigurationService, +// @IInstantiationService instantiationService: IInstantiationService, +// @ITextFileService private textFileService: ITextFileService, +// @IWindowService private windowService: IWindowService, +// @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService +// ) { +// super(stat => this.statToResource(stat), instantiationService); + +// this.toDispose = []; + +// this.updateDropEnablement(); + +// this.registerListeners(); +// } + +// private statToResource(stat: ExplorerItem): URI { +// if (stat.isDirectory) { +// return URI.from({ scheme: 'folder', path: stat.resource.path }); // indicates that we are dragging a folder +// } + +// return stat.resource; +// } + +// private registerListeners(): void { +// this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateDropEnablement())); +// } + +// private updateDropEnablement(): void { +// this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop'); +// } + +// public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void { +// const sources: ExplorerItem[] = data.getData(); +// if (sources && sources.length) { + +// // When dragging folders, make sure to collapse them to free up some space +// sources.forEach(s => { +// if (s.isDirectory && tree.isExpanded(s)) { +// tree.collapse(s, false); +// } +// }); + +// // Apply some datatransfer types to allow for dragging the element outside of the application +// this.instantiationService.invokeFunction(fillResourceDataTransfers, sources, originalEvent); + +// // The only custom data transfer we set from the explorer is a file transfer +// // to be able to DND between multiple code file explorers across windows +// const fileResources = sources.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); +// if (fileResources.length) { +// originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources)); +// } +// } +// } + +// public onDragOver(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): IDragOverReaction { +// if (!this.dropEnabled) { +// return DRAG_OVER_REJECT; +// } + +// const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); +// const fromDesktop = data instanceof DesktopDragAndDropData; + +// // Desktop DND +// if (fromDesktop) { +// const types: string[] = originalEvent.dataTransfer.types; +// const typesArray: string[] = []; +// for (let i = 0; i < types.length; i++) { +// typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase +// } + +// if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) { +// return DRAG_OVER_REJECT; +// } +// } + +// // Other-Tree DND +// else if (data instanceof ExternalElementsDragAndDropData) { +// return DRAG_OVER_REJECT; +// } + +// // In-Explorer DND +// else { +// const sources: ExplorerItem[] = data.getData(); +// if (target instanceof Model) { +// if (sources[0].isRoot) { +// return DRAG_OVER_ACCEPT_BUBBLE_DOWN(false); +// } + +// return DRAG_OVER_REJECT; +// } + +// if (!Array.isArray(sources)) { +// return DRAG_OVER_REJECT; +// } + +// if (sources.some((source) => { +// if (source instanceof NewStatPlaceholder) { +// return true; // NewStatPlaceholders can not be moved +// } + +// if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) { +// return true; // Root folder can not be moved to a non root file stat. +// } + +// if (source.resource.toString() === target.resource.toString()) { +// return true; // Can not move anything onto itself +// } + +// if (source.isRoot && target instanceof ExplorerItem && target.isRoot) { +// // Disable moving workspace roots in one another +// return false; +// } + +// if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) { +// return true; // Can not move a file to the same parent unless we copy +// } + +// if (resources.isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) { +// return true; // Can not move a parent folder into one of its children +// } + +// return false; +// })) { +// return DRAG_OVER_REJECT; +// } +// } + +// // All (target = model) +// if (target instanceof Model) { +// return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(false) : DRAG_OVER_REJECT; // can only drop folders to workspace +// } + +// // All (target = file/folder) +// else { +// if (target.isDirectory) { +// if (target.isReadonly) { +// return DRAG_OVER_REJECT; +// } +// return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true); +// } + +// if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) { +// return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_UP_COPY : DRAG_OVER_ACCEPT_BUBBLE_UP; +// } +// } + +// return DRAG_OVER_REJECT; +// } + +// public drop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): void { + +// // Desktop DND (Import file) +// if (data instanceof DesktopDragAndDropData) { +// this.handleExternalDrop(tree, data, target, originalEvent); +// } + +// // In-Explorer DND (Move/Copy file) +// else { +// this.handleExplorerDrop(tree, data, target, originalEvent); +// } +// } + +// private handleExternalDrop(tree: ITree, data: DesktopDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): TPromise { +// const droppedResources = extractResources(originalEvent.browserEvent as DragEvent, true); + +// // Check for dropped external files to be folders +// return this.fileService.resolveFiles(droppedResources).then(result => { + +// // Pass focus to window +// this.windowService.focusWindow(); + +// // Handle folders by adding to workspace if we are in workspace context +// const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource })); +// if (folders.length > 0) { + +// // If we are in no-workspace context, ask for confirmation to create a workspace +// let confirmedPromise: TPromise = Promise.resolve({ confirmed: true }); +// if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) { +// confirmedPromise = this.dialogService.confirm({ +// message: folders.length > 1 ? nls.localize('dropFolders', "Do you want to add the folders to the workspace?") : nls.localize('dropFolder', "Do you want to add the folder to the workspace?"), +// type: 'question', +// primaryButton: folders.length > 1 ? nls.localize('addFolders', "&&Add Folders") : nls.localize('addFolder', "&&Add Folder") +// }); +// } + +// return confirmedPromise.then(res => { +// if (res.confirmed) { +// return this.workspaceEditingService.addFolders(folders); +// } + +// return void 0; +// }); +// } + +// // Handle dropped files (only support FileStat as target) +// else if (target instanceof ExplorerItem && !target.isReadonly) { +// const addFilesAction = this.instantiationService.createInstance(AddFilesAction, tree, target, null); + +// return addFilesAction.run(droppedResources.map(res => res.resource)); +// } + +// return void 0; +// }); +// } + +// private handleExplorerDrop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): TPromise { +// const sources: ExplorerItem[] = resources.distinctParents(data.getData(), s => s.resource); +// const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); + +// let confirmPromise: TPromise; + +// // Handle confirm setting +// const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); +// if (confirmDragAndDrop) { +// confirmPromise = this.dialogService.confirm({ +// message: sources.length > 1 && sources.every(s => s.isRoot) ? nls.localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") +// : sources.length > 1 ? getConfirmMessage(nls.localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", sources.length), sources.map(s => s.resource)) +// : sources[0].isRoot ? nls.localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", sources[0].name) +// : nls.localize('confirmMove', "Are you sure you want to move '{0}'?", sources[0].name), +// checkbox: { +// label: nls.localize('doNotAskAgain', "Do not ask me again") +// }, +// type: 'question', +// primaryButton: nls.localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") +// }); +// } else { +// confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult); +// } + +// return confirmPromise.then(res => { + +// // Check for confirmation checkbox +// let updateConfirmSettingsPromise: TPromise = Promise.resolve(void 0); +// if (res.confirmed && res.checkboxChecked === true) { +// updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER); +// } + +// return updateConfirmSettingsPromise.then(() => { +// if (res.confirmed) { +// const rootDropPromise = this.doHandleRootDrop(sources.filter(s => s.isRoot), target); +// return Promise.all(sources.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(tree, source, target, isCopy)).concat(rootDropPromise)).then(() => void 0); +// } + +// return Promise.resolve(void 0); +// }); +// }); +// } + +// private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem | Model): TPromise { +// if (roots.length === 0) { +// return Promise.resolve(undefined); +// } + +// const folders = this.contextService.getWorkspace().folders; +// let targetIndex: number; +// const workspaceCreationData: IWorkspaceFolderCreationData[] = []; +// const rootsToMove: IWorkspaceFolderCreationData[] = []; + +// for (let index = 0; index < folders.length; index++) { +// const data = { +// uri: folders[index].uri +// }; +// if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) { +// targetIndex = workspaceCreationData.length; +// } + +// if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) { +// workspaceCreationData.push(data); +// } else { +// rootsToMove.push(data); +// } +// } +// if (target instanceof Model) { +// targetIndex = workspaceCreationData.length; +// } + +// workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); +// return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); +// } + +// private doHandleExplorerDrop(tree: ITree, source: ExplorerItem, target: ExplorerItem | Model, isCopy: boolean): TPromise { +// if (!(target instanceof ExplorerItem)) { +// return Promise.resolve(void 0); +// } + +// return tree.expand(target).then(() => { + +// if (target.isReadonly) { +// return void 0; +// } + +// // Reuse duplicate action if user copies +// if (isCopy) { +// return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run(); +// } + +// // Otherwise move +// const targetResource = resources.joinPath(target.resource, source.name); + +// return this.textFileService.move(source.resource, targetResource).then(void 0, error => { + +// // Conflict +// if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { +// const confirm: IConfirmation = { +// message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), +// detail: nls.localize('irreversible', "This action is irreversible!"), +// primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), +// type: 'warning' +// }; + +// // Move with overwrite if the user confirms +// return this.dialogService.confirm(confirm).then(res => { +// if (res.confirmed) { +// return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(void 0, error => this.notificationService.error(error)); +// } + +// return void 0; +// }); +// } + +// // Any other error +// else { +// this.notificationService.error(error); +// } + +// return void 0; +// }); +// }, errors.onUnexpectedError); +// } +// } From a6b893c6a34199b91c010838387f07913200d1ee Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 11:06:30 +0100 Subject: [PATCH 02/65] explorer view height 100% --- .../parts/files/electron-browser/media/explorerviewlet.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index c3fc2504f70..fa284e26109 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -9,7 +9,8 @@ } /* --- Explorer viewlet --- */ -.explorer-viewlet { +.explorer-viewlet, +.explorer-folders-view { height: 100%; } From 3a935fa495c4842023a003526be74785c1cea29f Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 11:23:09 +0100 Subject: [PATCH 03/65] explorer: slight reorder and polish --- .../electron-browser/views/explorerView.ts | 195 +++++++++--------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index d6453fc9c88..d5d7ae3d792 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -55,12 +55,10 @@ export interface IExplorerViewOptions extends IViewletViewOptions { export class ExplorerView extends ViewletPanel implements IExplorerView { - public static readonly ID: string = 'workbench.explorer.fileView'; + static readonly ID: string = 'workbench.explorer.fileView'; private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes - public readonly id: string = ExplorerView.ID; - private tree: WorkbenchAsyncDataTree; private filter: FilesFilter; private isCreated: boolean; @@ -73,14 +71,12 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private readonlyContext: IContextKey; private rootContext: IContextKey; - private fileEventsFilter: ResourceGlobMatcher; - private shouldRefresh: boolean; - private autoReveal: boolean; private sortOrder: SortOrder; private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; - private isDisposed: boolean; + private autoReveal = false; + private isDisposed = false; constructor( options: IExplorerViewOptions, @@ -102,11 +98,9 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { @IMenuService private menuService: IMenuService, @IClipboardService private clipboardService: IClipboardService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); this._editableExplorerItems = new EditableExplorerItems(); - this.autoReveal = true; - this.explorerRefreshDelayer = new ThrottledDelayer(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY); this.resourceContext = instantiationService.createInstance(ResourceContextKey); @@ -115,12 +109,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService); this.rootContext = ExplorerRootContext.bindTo(contextKeyService); - this.fileEventsFilter = instantiationService.createInstance( - ResourceGlobMatcher, - (root: URI) => this.getFileEventsExcludes(root), - (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) - ); - this.decorationProvider = new ExplorerDecorationsProvider(this.model, contextService); decorationService.registerDecorationsProvider(this.decorationProvider); this.disposables.push(this.decorationProvider); @@ -173,6 +161,38 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return this._editableExplorerItems; } + // Memoized locals + @memoize private get fileEventsFilter(): ResourceGlobMatcher { + const fileEventsFilter = this.instantiationService.createInstance( + ResourceGlobMatcher, + (root: URI) => this.getFileEventsExcludes(root), + (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) + ); + this.disposables.push(fileEventsFilter); + + return fileEventsFilter; + } + + @memoize private get contributedContextMenu(): IMenu { + const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService); + this.disposables.push(contributedContextMenu); + return contributedContextMenu; + } + + @memoize private get fileCopiedContextKey(): IContextKey { + return FileCopiedContext.bindTo(this.contextKeyService); + } + + @memoize + private get model(): Model { + const model = this.instantiationService.createInstance(Model); + this.disposables.push(model); + + return model; + } + + // Split view methods + render(): void { super.render(); @@ -260,36 +280,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } - private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { - if (this.isDisposed) { - return; // guard against possible race condition when config change causes recreate of views - } - - this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal; - - // Push down config updates to components of viewer - let needsRefresh = false; - if (this.filter) { - needsRefresh = this.filter.updateConfiguration(); - } - - const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; - if (this.sortOrder !== configSortOrder) { - this.sortOrder = configSortOrder; - needsRefresh = true; - } - - if (event && !needsRefresh) { - needsRefresh = event.affectsConfiguration('explorer.decorations.colors') - || event.affectsConfiguration('explorer.decorations.badges'); - } - - // Refresh viewer as needed if this originates from a config event - if (event && needsRefresh) { - this.doRefresh(); - } - } - focus(): void { super.focus(); @@ -365,10 +355,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } - collapseAll(): void { - this.tree.collapseAll(); - } - private openFocusedElement(preserveFocus?: boolean): void { const focusedElements = this.tree.getFocus(); const stat = focusedElements && focusedElements.length ? focusedElements[0] : undefined; @@ -389,14 +375,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return toResource(input, { supportSideBySide: true }); } - @memoize - private get model(): Model { - const model = this.instantiationService.createInstance(Model); - this.disposables.push(model); - - return model; - } - private createTree(container: HTMLElement): void { // TODO@isidor missing features // const controller = this.instantiationService.createInstance(FileController); @@ -422,6 +400,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { filter: this.filter }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); + this.disposables.push(this.tree); filesRenderer.acquireTree(this.tree); // Bind context keys FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); @@ -465,6 +444,38 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return DOM.getLargestChildWidth(parentNode, childNodes); } + // React on events + + private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { + if (this.isDisposed) { + return; // guard against possible race condition when config change causes recreate of views + } + + this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal; + + // Push down config updates to components of viewer + let needsRefresh = false; + if (this.filter) { + needsRefresh = this.filter.updateConfiguration(); + } + + const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; + if (this.sortOrder !== configSortOrder) { + this.sortOrder = configSortOrder; + needsRefresh = true; + } + + if (event && !needsRefresh) { + needsRefresh = event.affectsConfiguration('explorer.decorations.colors') + || event.affectsConfiguration('explorer.decorations.badges'); + } + + // Refresh viewer as needed if this originates from a config event + if (event && needsRefresh) { + this.doRefresh(); + } + } + private onFileOperation(e: FileOperationEvent): void { if (!this.isCreated) { return; // ignore if not yet created @@ -703,6 +714,36 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } + private onContextMenu(e: ITreeContextMenuEvent): void { + const stat = e.element; + if (stat instanceof NewStatPlaceholder) { + return; + } + + // update dynamic contexts + this.fileCopiedContextKey.set(this.clipboardService.hasResources()); + + const selection = this.tree.getSelection(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => { + const actions: IAction[] = []; + fillInContextMenuActions(this.contributedContextMenu, { arg: stat instanceof ExplorerItem ? stat.resource : {}, shouldForwardArgs: true }, actions, this.contextMenuService); + return actions; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree.domFocus(); + } + }, + getActionsContext: () => selection && selection.indexOf(stat) >= 0 + ? selection.map((fs: ExplorerItem) => fs.resource) + : stat instanceof ExplorerItem ? [stat.resource] : [] + }); + } + + // General methods + /** * Refresh the contents of the explorer to get up to date data from the disk about the file structure. */ @@ -932,42 +973,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } - private onContextMenu(e: ITreeContextMenuEvent): void { - const stat = e.element; - if (stat instanceof NewStatPlaceholder) { - return; - } - - // update dynamic contexts - this.fileCopiedContextKey.set(this.clipboardService.hasResources()); - - const selection = this.tree.getSelection(); - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => { - const actions: IAction[] = []; - fillInContextMenuActions(this.contributedContextMenu, { arg: stat instanceof ExplorerItem ? stat.resource : {}, shouldForwardArgs: true }, actions, this.contextMenuService); - return actions; - }, - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - this.tree.domFocus(); - } - }, - getActionsContext: () => selection && selection.indexOf(stat) >= 0 - ? selection.map((fs: ExplorerItem) => fs.resource) - : stat instanceof ExplorerItem ? [stat.resource] : [] - }); - } - - @memoize private get contributedContextMenu(): IMenu { - const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService); - this.disposables.push(contributedContextMenu); - return contributedContextMenu; - } - - @memoize private get fileCopiedContextKey(): IContextKey { - return FileCopiedContext.bindTo(this.contextKeyService); + collapseAll(): void { + this.tree.collapseAll(); } dispose(): void { From 4a8758b60ba639e93dccaecb7eb422a13ff81798 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 11:33:45 +0100 Subject: [PATCH 04/65] explorer: memoize actions and some polish --- .../electron-browser/views/explorerView.ts | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index d5d7ae3d792..5c645cd535a 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -53,6 +53,13 @@ export interface IExplorerViewOptions extends IViewletViewOptions { fileViewletState: EditableExplorerItems; } +function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): glob.IExpression { + const scope = root ? { resource: root } : void 0; + const configuration = configurationService.getValue(scope); + + return (configuration && configuration.files && configuration.files.exclude) || Object.create(null); +} + export class ExplorerView extends ViewletPanel implements IExplorerView { static readonly ID: string = 'workbench.explorer.fileView'; @@ -115,13 +122,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.disposables.push(this.resourceContext); } - private getFileEventsExcludes(root?: URI): glob.IExpression { - const scope = root ? { resource: root } : void 0; - const configuration = this.configurationService.getValue(scope); - - return (configuration && configuration.files && configuration.files.exclude) || Object.create(null); - } - protected renderHeader(container: HTMLElement): void { super.renderHeader(container); @@ -165,7 +165,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { @memoize private get fileEventsFilter(): ResourceGlobMatcher { const fileEventsFilter = this.instantiationService.createInstance( ResourceGlobMatcher, - (root: URI) => this.getFileEventsExcludes(root), + (root: URI) => getFileEventsExcludes(this.configurationService, root), (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) ); this.disposables.push(fileEventsFilter); @@ -230,7 +230,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { })); } - getActions(): IAction[] { + @memoize getActions(): IAction[] { const actions: Action[] = []; actions.push(this.instantiationService.createInstance(NewFileAction, this.tree, this._editableExplorerItems, null)); @@ -290,7 +290,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (this.autoReveal) { const selection = this.tree.getSelection(); if (selection.length > 0) { - this.reveal(selection[0], 0.5); + this.tree.reveal(selection[0], 0.5); } } @@ -506,7 +506,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // Refresh the Parent (View) this.tree.refresh(p).then(() => { // Reveal and focus new element - this.reveal(childElement, 0.5); + this.tree.reveal(childElement, 0.5); this.tree.setFocus([childElement]); }); }); @@ -703,7 +703,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.explorerRefreshDelayer.trigger(() => { return this.doRefresh().then(() => { if (newRoots.length === 1) { - return this.reveal(this.model.findClosest(newRoots[0].uri), 0.5); + return this.tree.reveal(this.model.findClosest(newRoots[0].uri), 0.5); } return undefined; @@ -900,7 +900,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { const selection = this.hasSingleSelection(resource); if (selection) { if (reveal) { - this.reveal(selection, 0.5); + this.tree.reveal(selection, 0.5); } return; @@ -957,7 +957,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // } if (reveal) { - this.reveal(fileStat, 0.5); + this.tree.reveal(fileStat, 0.5); } if (!fileStat.isDirectory) { @@ -967,12 +967,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.tree.setFocus([fileStat]); } - private reveal(element: any, relativeTop?: number): void { - if (this.tree) { - this.tree.reveal(element, relativeTop); - } - } - collapseAll(): void { this.tree.collapseAll(); } From 05cf208347c122a320765b83602dca23f893824a Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 11:36:32 +0100 Subject: [PATCH 05/65] explorer: do not memoize actions, that was too optimisitc --- .../parts/files/electron-browser/views/explorerView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 5c645cd535a..161f0bded74 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -230,7 +230,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { })); } - @memoize getActions(): IAction[] { + getActions(): IAction[] { const actions: Action[] = []; actions.push(this.instantiationService.createInstance(NewFileAction, this.tree, this._editableExplorerItems, null)); From 5ee32f1796ebb3c6d18f64a291c687dac9397628 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 11:58:55 +0100 Subject: [PATCH 06/65] explorer: when selecting a stat make sure to expand all elements in the parent chain --- .../electron-browser/views/explorerView.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 161f0bded74..017ab546f53 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; -import { ThrottledDelayer } from 'vs/base/common/async'; +import { ThrottledDelayer, sequence, ignoreErrors } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import * as glob from 'vs/base/common/glob'; @@ -906,7 +906,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return; } - // First try to get the stat object from the input to avoid a roundtrip if (!this.isCreated) { return; } @@ -942,29 +941,30 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { : undefined; } - private doSelect(fileStat: ExplorerItem, reveal: boolean): void { + private doSelect(fileStat: ExplorerItem, reveal: boolean): Promise { if (!fileStat) { - return; + return Promise.resolve(void 0); } - // // Special case: we are asked to reveal and select an element that is not visible - // // In this case we take the parent element so that we are at least close to it. - // if (!this.tree.isVisible(fileStat)) { - // fileStat = fileStat.parent; - // if (!fileStat) { - // return; - // } - // } - - if (reveal) { - this.tree.reveal(fileStat, 0.5); + // Expand all stats in the parent chain + const toExpand: ExplorerItem[] = []; + let parent = fileStat.parent; + while (parent) { + toExpand.push(parent); + parent = parent.parent; } - if (!fileStat.isDirectory) { - this.tree.setSelection([fileStat]); // Since folders can not be opened, only select files - } + return sequence(toExpand.reverse().map(s => () => ignoreErrors(this.tree.expand(s)))).then(() => { + if (reveal) { + this.tree.reveal(fileStat, 0.5); + } - this.tree.setFocus([fileStat]); + if (!fileStat.isDirectory) { + this.tree.setSelection([fileStat]); // Since folders can not be opened, only select files + } + + this.tree.setFocus([fileStat]); + }); } collapseAll(): void { From ed752d077247e2e4b9246fa72dcfb1addcfb7239 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 12:27:38 +0100 Subject: [PATCH 07/65] explorer: properly open elements on user selection --- .../electron-browser/views/explorerView.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 017ab546f53..2e53f8259fc 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -38,7 +38,7 @@ import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/l import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, EditableExplorerItems as EditableExplorerItems, FilesFilter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; @@ -183,8 +183,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return FileCopiedContext.bindTo(this.contextKeyService); } - @memoize - private get model(): Model { + @memoize private get model(): Model { const model = this.instantiationService.createInstance(Model); this.disposables.push(model); @@ -424,12 +423,18 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // Open when selecting via keyboard this.disposables.push(this.tree.onDidChangeSelection(e => { const selection = e.elements; - if (selection && selection.length) { - // TODO@Isidor check this, and add side by side, focus passing - const stat = selection[0]; + // Do not react if the user is expanding selection + if (selection && selection.length === 1) { + let isDoubleClick = false; + let sideBySide = false; + if (e.browserEvent instanceof MouseEvent) { + isDoubleClick = e.browserEvent.detail === 2; + sideBySide = this.tree.useAltAsMultipleSelectionModifier ? (e.browserEvent.ctrlKey || e.browserEvent.metaKey) : e.browserEvent.altKey; + } - if (!stat.isDirectory) { - this.editorService.openEditor({ resource: stat.resource }); + if (!selection[0].isDirectory) { + // Pass focus for keyboard events and for double click + this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: !isDoubleClick, pinned: isDoubleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } } })); From ab0b87329f1e5c89f1df6e50c5a4b7e7a37006dc Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 16:58:14 +0100 Subject: [PATCH 08/65] explorer: send telemetry when file is opened --- .../files/electron-browser/views/explorerView.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 2e53f8259fc..daa3fc21e2a 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -48,6 +48,7 @@ import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export interface IExplorerViewOptions extends IViewletViewOptions { fileViewletState: EditableExplorerItems; @@ -103,7 +104,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { @IThemeService private themeService: IWorkbenchThemeService, @IListService private listService: IListService, @IMenuService private menuService: IMenuService, - @IClipboardService private clipboardService: IClipboardService + @IClipboardService private clipboardService: IClipboardService, + @ITelemetryService private telemetryService: ITelemetryService ) { super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); @@ -434,6 +436,12 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (!selection[0].isDirectory) { // Pass focus for keyboard events and for double click + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + }*/ + this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: !isDoubleClick, pinned: isDoubleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } } @@ -541,7 +549,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { const modelElements = this.model.findAll(oldResource); modelElements.forEach(modelElement => { //Check if element is expanded - isExpanded = this.tree.isExpanded(modelElement); + isExpanded = !this.tree.isCollapsed(modelElement); // Rename File (Model) modelElement.rename(newElement); From b25d7e2b267ba25e1ed84e8880048b2aac29d665 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 14 Dec 2018 17:29:44 +0100 Subject: [PATCH 09/65] explorer: some notes and clenaup --- .../files/electron-browser/views/explorerView.ts | 7 ------- .../files/electron-browser/views/explorerViewer.ts | 12 +++--------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index daa3fc21e2a..51dbf05de65 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -377,12 +377,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } private createTree(container: HTMLElement): void { - // TODO@isidor missing features - // const controller = this.instantiationService.createInstance(FileController); - // this.disposables.push(controller); - // const sorter = this.instantiationService.createInstance(FileSorter); - // this.disposables.push(sorter); - // const dnd = this.instantiationService.createInstance(FileDragAndDrop); this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); const filesRenderer = this.instantiationService.createInstance(FilesRenderer, this._editableExplorerItems); @@ -402,7 +396,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.disposables.push(this.tree); - filesRenderer.acquireTree(this.tree); // Bind context keys FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); ExplorerFocusedContext.bindTo(this.tree.contextKeyService); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index c0726ee7efd..d5e9db666ed 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -32,7 +32,6 @@ import { once } from 'vs/base/common/functional'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { normalize, join, nativeSep } from 'vs/base/common/paths'; import { rtrim } from 'vs/base/common/strings'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; @@ -147,7 +146,6 @@ export class FilesRenderer implements ITreeRenderer; constructor( state: EditableExplorerItems, @@ -171,10 +169,6 @@ export class FilesRenderer implements ITreeRenderer): void { - this.tree = tree; - } - renderTemplate(container: HTMLElement): IFileTemplateData { const elementDisposable = Disposable.None; const label = this.instantiationService.createInstance(FileLabel, container, void 0); @@ -244,18 +238,18 @@ export class FilesRenderer implements ITreeRenderer 0 && !stat.isDirectory ? lastDot : value.length }); inputBox.focus(); - const done = once((commit: boolean, blur: boolean) => { + const done = once(async (commit: boolean, blur: boolean) => { label.element.style.display = 'none'; if (stat instanceof NewStatPlaceholder) { stat.destroy(); } if (commit && inputBox.value) { - editableData.action.run({ value: inputBox.value }); + await editableData.action.run({ value: inputBox.value }); } dispose(toDispose); container.removeChild(label.element); - this.tree.refresh(stat.parent).then(() => this.tree.domFocus()); + // todo@isidor need to unset editable data }); const toDispose = [ From 39b480b67ec8921cbb3db91561c41e3cfc92ec72 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 17 Dec 2018 11:04:51 +0100 Subject: [PATCH 10/65] fix compile error, IDataSource moved to another file --- .../parts/files/electron-browser/views/explorerViewer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index d5e9db666ed..08bbb579731 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -8,7 +8,6 @@ import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IDataSource } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IFileService, FileKind } from 'vs/platform/files/common/files'; @@ -17,7 +16,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IDataSource } from 'vs/base/browser/ui/tree/tree'; import { IFileViewletState, IEditableData } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; From 8899a0cb34556528b0ebe1c8829081610a74a7ae Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 17 Dec 2018 15:45:31 +0100 Subject: [PATCH 11/65] explorer: introduce ExplorerService --- src/vs/workbench/parts/files/browser/files.ts | 3 +- .../{explorerModel.ts => explorerService.ts} | 57 +++--------- src/vs/workbench/parts/files/common/files.ts | 87 +++++++++++++------ .../files/electron-browser/fileActions.ts | 12 +-- .../electron-browser/files.contribution.ts | 6 +- .../views/explorerDecorationsProvider.ts | 6 +- .../electron-browser/views/explorerView.ts | 54 +++++------- .../electron-browser/views/explorerViewer.ts | 13 +-- .../electron-browser/views/openEditorsView.ts | 3 +- ...rModel.test.ts => explorerService.test.ts} | 2 +- 10 files changed, 119 insertions(+), 124 deletions(-) rename src/vs/workbench/parts/files/common/{explorerModel.ts => explorerService.ts} (91%) rename src/vs/workbench/parts/files/test/electron-browser/{explorerModel.test.ts => explorerService.test.ts} (99%) diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index 00f7804d9df..ac5b43d9cca 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -5,7 +5,8 @@ import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; -import { ExplorerItem, OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; +import { OpenEditor } from 'vs/workbench/parts/files/common/files'; import { toResource } from 'vs/workbench/common/editor'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { List } from 'vs/base/browser/ui/list/listWidget'; diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerService.ts similarity index 91% rename from src/vs/workbench/parts/files/common/explorerModel.ts rename to src/vs/workbench/parts/files/common/explorerService.ts index 4f101f1d1b4..0c9301293fd 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerService.ts @@ -4,20 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; import { isLinux } from 'vs/base/common/platform'; import { IFileStat } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { toResource, IEditorIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; -import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { coalesce } from 'vs/base/common/arrays'; +import { IExplorerService } from 'vs/workbench/parts/files/common/files'; -export class Model { +export class ExplorerService implements IExplorerService { + _serviceBrand: any; + + setEditable(stat: ExplorerItem, editable: boolean): void { + throw new Error('Method not implemented.'); + } + + onDidEditStat: Event; private _roots: ExplorerItem[]; private _listener: IDisposable; @@ -450,46 +456,3 @@ export class NewStatPlaceholder extends ExplorerItem { return child; } } - -export class OpenEditor implements IEditorIdentifier { - - constructor(private _editor: IEditorInput, private _group: IEditorGroup) { - // noop - } - - public get editor() { - return this._editor; - } - - public get editorIndex() { - return this._group.getIndexOfEditor(this.editor); - } - - public get group() { - return this._group; - } - - public get groupId() { - return this._group.id; - } - - public getId(): string { - return `openeditor:${this.groupId}:${this.editorIndex}:${this.editor.getName()}:${this.editor.getDescription()}`; - } - - public isPreview(): boolean { - return this._group.previewEditor === this.editor; - } - - public isUntitled(): boolean { - return !!toResource(this.editor, { supportSideBySide: true, filter: Schemas.untitled }); - } - - public isDirty(): boolean { - return this.editor.isDirty(); - } - - public getResource(): URI | null { - return toResource(this.editor, { supportSideBySide: true }); - } -} diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index a5e27003514..4ef69ff73e3 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -5,13 +5,13 @@ import { URI } from 'vs/base/common/uri'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorInput, toResource } from 'vs/workbench/common/editor'; import { IFilesConfiguration, FileChangeType, IFileService } from 'vs/platform/files/common/files'; -import { ExplorerItem, OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; +import { Event } from 'vs/base/common/event'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -20,6 +20,9 @@ import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views'; import { Schemas } from 'vs/base/common/network'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; +import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; /** * Explorer viewlet id. @@ -38,6 +41,17 @@ export interface IExplorerView { select(resource: URI, reveal?: boolean): void; } +export interface IExplorerService { + _serviceBrand: any; + readonly roots: ExplorerItem[]; + readonly onDidEditStat: Event; + + setEditable(stat: ExplorerItem, editable: boolean): void; + findClosest(resource: URI): ExplorerItem | null; + findAll(resource: URI): ExplorerItem[]; +} +export const IExplorerService = createDecorator('explorerService'); + /** * Context Keys to use with keybindings for the Explorer and Open Editors view */ @@ -107,32 +121,6 @@ export interface IFileResource { isDirectory?: boolean; } -/** - * Helper to get an explorer item from an object. - */ -export function explorerItemToFileResource(obj: ExplorerItem | OpenEditor): IFileResource | null { - if (obj instanceof ExplorerItem) { - const stat = obj as ExplorerItem; - - return { - resource: stat.resource, - isDirectory: stat.isDirectory - }; - } - - if (obj instanceof OpenEditor) { - const editor = obj as OpenEditor; - const resource = editor.getResource(); - if (resource) { - return { - resource - }; - } - } - - return null; -} - export const SortOrderConfiguration = { DEFAULT: 'default', MIXED: 'mixed', @@ -210,3 +198,46 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { this.fileWatcher = dispose(this.fileWatcher); } } + +export class OpenEditor implements IEditorIdentifier { + + constructor(private _editor: IEditorInput, private _group: IEditorGroup) { + // noop + } + + public get editor() { + return this._editor; + } + + public get editorIndex() { + return this._group.getIndexOfEditor(this.editor); + } + + public get group() { + return this._group; + } + + public get groupId() { + return this._group.id; + } + + public getId(): string { + return `openeditor:${this.groupId}:${this.editorIndex}:${this.editor.getName()}:${this.editor.getDescription()}`; + } + + public isPreview(): boolean { + return this._group.previewEditor === this.editor; + } + + public isUntitled(): boolean { + return !!toResource(this.editor, { supportSideBySide: true, filter: Schemas.untitled }); + } + + public isDirty(): boolean { + return this.editor.isDirty(); + } + + public getResource(): URI | null { + return toResource(this.editor, { supportSideBySide: true }); + } +} diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index a84928487e4..a9e107c0ab0 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -21,7 +21,6 @@ import { VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; -import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -49,6 +48,7 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { EditableExplorerItems } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { NewStatPlaceholder, ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; export interface IEditableData { action: IAction; @@ -767,8 +767,9 @@ export class AddFilesAction extends BaseFileAction { if (this.element) { targetElement = this.element; } else { - const input: ExplorerItem | Model = this.tree.getInput(); - targetElement = this.tree.getFocus() || (input instanceof Model ? input.roots[0] : input); + // TODO@isidor + // const input: ExplorerItem | Model = this.tree.getInput(); + // targetElement = this.tree.getFocus() || (input instanceof Model ? input.roots[0] : input); } if (!targetElement.isDirectory) { @@ -903,8 +904,9 @@ class PasteFileAction extends BaseFileAction { this.tree = tree; this.element = element; if (!this.element) { - const input: ExplorerItem | Model = this.tree.getInput(); - this.element = input instanceof Model ? input.roots[0] : input; + // TODO@isidor + // const input: ExplorerItem | Model = this.tree.getInput(); + // this.element = input instanceof Model ? input.roots[0] : input; } this._updateEnablement(); } diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index f857f474d90..451b36ca422 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -13,7 +13,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, SUPPORTED_ENCODINGS } from 'vs/platform/files/common/files'; -import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { FileEditorTracker } from 'vs/workbench/parts/files/browser/editors/fileEditorTracker'; import { SaveErrorHandler } from 'vs/workbench/parts/files/electron-browser/saveErrorHandler'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; @@ -35,6 +35,8 @@ import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorG import { ILabelService } from 'vs/platform/label/common/label'; import { nativeSep } from 'vs/base/common/paths'; import { IPartService } from 'vs/workbench/services/part/common/partService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ExplorerService } from 'vs/workbench/parts/files/common/explorerService'; // Viewlet Action export class OpenExplorerViewletAction extends ShowViewletAction { @@ -79,6 +81,8 @@ Registry.as(ViewletExtensions.Viewlets).registerViewlet(new Vie 0 )); +registerSingleton(IExplorerService, ExplorerService); + Registry.as(ViewletExtensions.Viewlets).setDefaultViewletId(VIEWLET_ID); const openViewletKb: IKeybindings = { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts index 2fcb0fa5e2a..e4fc759afaa 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts @@ -6,12 +6,12 @@ import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; -import { Model } from 'vs/workbench/parts/files/common/explorerModel'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vscode-xterm'; import { dispose } from 'vs/base/common/lifecycle'; +import { IExplorerService } from 'vs/workbench/parts/files/common/files'; export class ExplorerDecorationsProvider implements IDecorationsProvider { readonly label: string = localize('label', "Explorer"); @@ -19,7 +19,7 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { private toDispose: IDisposable[]; constructor( - private model: Model, + @IExplorerService private explorerService: IExplorerService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { this.toDispose = []; @@ -37,7 +37,7 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { } provideDecorations(resource: URI): IDecorationData | undefined { - const fileStat = this.model.findClosest(resource); + const fileStat = this.explorerService.findClosest(resource); if (fileStat && fileStat.isRoot && fileStat.isError) { return { tooltip: localize('canNotResolve', "Can not resolve workspace folder"), diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 51dbf05de65..045a3083d6b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -12,7 +12,7 @@ import * as resources from 'vs/base/common/resources'; import * as glob from 'vs/base/common/glob'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext } from 'vs/workbench/parts/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileService, FILES_EXCLUDE_CONFIG, IFileStat } from 'vs/platform/files/common/files'; import { RefreshViewExplorerAction, NewFolderAction, NewFileAction, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; @@ -20,7 +20,6 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as DOM from 'vs/base/browser/dom'; import { CollapseAction2 } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { ExplorerDecorationsProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -49,6 +48,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; export interface IExplorerViewOptions extends IViewletViewOptions { fileViewletState: EditableExplorerItems; @@ -105,7 +105,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { @IListService private listService: IListService, @IMenuService private menuService: IMenuService, @IClipboardService private clipboardService: IClipboardService, - @ITelemetryService private telemetryService: ITelemetryService + @ITelemetryService private telemetryService: ITelemetryService, + @IExplorerService private explorerService: IExplorerService ) { super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); @@ -118,7 +119,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService); this.rootContext = ExplorerRootContext.bindTo(contextKeyService); - this.decorationProvider = new ExplorerDecorationsProvider(this.model, contextService); + this.decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService); decorationService.registerDecorationsProvider(this.decorationProvider); this.disposables.push(this.decorationProvider); this.disposables.push(this.resourceContext); @@ -185,13 +186,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return FileCopiedContext.bindTo(this.contextKeyService); } - @memoize private get model(): Model { - const model = this.instantiationService.createInstance(Model); - this.disposables.push(model); - - return model; - } - // Split view methods render(): void { @@ -383,7 +377,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.disposables.push(filesRenderer); this.tree = new WorkbenchAsyncDataTree(container, new ExplorerDelegate(), [filesRenderer], - this.instantiationService.createInstance(ExplorerDataSource, this.model), { + this.instantiationService.createInstance(ExplorerDataSource), { accessibilityProvider: new ExplorerAccessibilityProvider(), ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), identityProvider: { @@ -491,7 +485,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (e.operation === FileOperation.CREATE || e.operation === FileOperation.COPY) { const addedElement = e.target; const parentResource = resources.dirname(addedElement.resource); - const parents = this.model.findAll(parentResource); + const parents = this.explorerService.findAll(parentResource); if (parents.length) { @@ -539,7 +533,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { let isExpanded = false; // Handle Rename if (oldParentResource && newParentResource && oldParentResource.toString() === newParentResource.toString()) { - const modelElements = this.model.findAll(oldResource); + const modelElements = this.explorerService.findAll(oldResource); modelElements.forEach(modelElement => { //Check if element is expanded isExpanded = !this.tree.isCollapsed(modelElement); @@ -563,8 +557,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // Handle Move else if (oldParentResource && newParentResource) { - const newParents = this.model.findAll(newParentResource); - const modelElements = this.model.findAll(oldResource); + const newParents = this.explorerService.findAll(newParentResource); + const modelElements = this.explorerService.findAll(oldResource); if (newParents.length && modelElements.length) { @@ -585,7 +579,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // Delete else if (e.operation === FileOperation.DELETE) { - const modelElements = this.model.findAll(e.resource); + const modelElements = this.explorerService.findAll(e.resource); modelElements.forEach(element => { if (element.parent) { const parent = element.parent; @@ -643,8 +637,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } // Compute if parent is visible and added file not yet part of it - const parentStat = this.model.findClosest(parent); - if (parentStat && parentStat.isDirectoryResolved && !this.model.findClosest(change.resource)) { + const parentStat = this.explorerService.findClosest(parent); + if (parentStat && parentStat.isDirectoryResolved && !this.explorerService.findClosest(change.resource)) { return true; } @@ -663,7 +657,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { for (let j = 0; j < deleted.length; j++) { const del = deleted[j]; - if (this.model.findClosest(del.resource)) { + if (this.explorerService.findClosest(del.resource)) { return true; } } @@ -677,7 +671,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { for (let j = 0; j < updated.length; j++) { const upd = updated[j]; - if (this.model.findClosest(upd.resource)) { + if (this.explorerService.findClosest(upd.resource)) { return true; } } @@ -709,7 +703,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this.explorerRefreshDelayer.trigger(() => { return this.doRefresh().then(() => { if (newRoots.length === 1) { - return this.tree.reveal(this.model.findClosest(newRoots[0].uri), 0.5); + return this.tree.reveal(this.explorerService.findClosest(newRoots[0].uri), 0.5); } return undefined; @@ -783,7 +777,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } private doRefresh(): Promise { - const targetsToResolve = this.model.roots.map(root => ({ root, resource: root.resource, options: { resolveTo: [] } })); + const targetsToResolve = this.explorerService.roots.map(root => ({ root, resource: root.resource, options: { resolveTo: [] } })); // First time refresh: Receive target through active editor input or selection and also include settings from previous session if (!this.isCreated) { @@ -843,8 +837,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { }); // Subsequent refresh: Merge stat into our local model and refresh tree modelStats.forEach((modelStat, index) => { - if (index < this.model.roots.length) { - ExplorerItem.mergeLocalWithDisk(modelStat, this.model.roots[index]); + if (index < this.explorerService.roots.length) { + ExplorerItem.mergeLocalWithDisk(modelStat, this.explorerService.roots[index]); } }); @@ -857,8 +851,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { .then(result => result.isDirectory ? ExplorerItem.create(result, target.root, target.options.resolveTo) : errorRoot(target.resource, target.root), () => errorRoot(target.resource, target.root)) .then(modelStat => { // Subsequent refresh: Merge stat into our local model and refresh tree - if (index < this.model.roots.length) { - ExplorerItem.mergeLocalWithDisk(modelStat, this.model.roots[index]); + if (index < this.explorerService.roots.length) { + ExplorerItem.mergeLocalWithDisk(modelStat, this.explorerService.roots[index]); } return this.tree.refresh(null); @@ -916,7 +910,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return; } - const fileStat = this.model.findClosest(resource); + const fileStat = this.explorerService.findClosest(resource); if (fileStat) { this.doSelect(fileStat, reveal); return; @@ -925,11 +919,11 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // Stat needs to be resolved first and then revealed const options: IResolveFileOptions = { resolveTo: [resource] }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - const rootUri = workspaceFolder ? workspaceFolder.uri : this.model.roots[0].resource; + const rootUri = workspaceFolder ? workspaceFolder.uri : this.explorerService.roots[0].resource; this.fileService.resolveFile(rootUri, options).then(stat => { // Convert to model - const root = this.model.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); + const root = this.explorerService.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); const modelStat = ExplorerItem.create(stat, root, options.resolveTo); // Update Input with disk Stat ExplorerItem.mergeLocalWithDisk(modelStat, root); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 08bbb579731..3a6fb501de0 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -6,7 +6,6 @@ import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; -import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -22,7 +21,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfiguration } from 'vs/workbench/parts/files/common/files'; +import { IFilesConfiguration, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { dirname, joinPath, basename } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; @@ -33,6 +32,7 @@ import { normalize, join, nativeSep } from 'vs/base/common/paths'; import { rtrim } from 'vs/base/common/strings'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; +import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -50,7 +50,7 @@ export class ExplorerDelegate implements IListVirtualDelegate { export class ExplorerDataSource implements IDataSource { constructor( - private model: Model, + @IExplorerService private explorerService: IExplorerService, @IProgressService private progressService: IProgressService, @INotificationService private notificationService: INotificationService, @IFileService private fileService: IFileService, @@ -64,11 +64,12 @@ export class ExplorerDataSource implements IDataSource { getChildren(element: ExplorerItem | null): Promise { if (element === null) { - if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || this.model.roots[0].isError) { + const roots = this.explorerService.roots; + if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) { // Display roots only when multi folder workspace - return Promise.resolve(this.model.roots); + return Promise.resolve(roots); } - element = this.model.roots[0]; + element = roots[0]; } // Return early if stat is already resolved diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 400f005c7d4..983c13fedd7 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -14,9 +14,8 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorInput } from 'vs/workbench/common/editor'; import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; -import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration } from 'vs/workbench/parts/files/common/files'; +import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; -import { OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { CloseAllEditorsAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/toggleEditorLayout'; diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts similarity index 99% rename from src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts rename to src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts index b4133dfedd2..3b03d9a38b2 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts @@ -9,7 +9,7 @@ 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/electron-browser/fileActions'; -import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { return new ExplorerItem(toResource(path), undefined, false, false, isFolder, name, mtime); From 7e8905b834a64a490f1a01dc9e8edd7cd3ad922a Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 17 Dec 2018 15:53:12 +0100 Subject: [PATCH 12/65] explorer service: do not use public as keyword --- .../parts/files/common/explorerService.ts | 82 ++++++++++--------- src/vs/workbench/parts/files/common/files.ts | 2 +- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerService.ts b/src/vs/workbench/parts/files/common/explorerService.ts index 0c9301293fd..bd079b58e14 100644 --- a/src/vs/workbench/parts/files/common/explorerService.ts +++ b/src/vs/workbench/parts/files/common/explorerService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; @@ -19,13 +19,8 @@ import { IExplorerService } from 'vs/workbench/parts/files/common/files'; export class ExplorerService implements IExplorerService { _serviceBrand: any; - setEditable(stat: ExplorerItem, editable: boolean): void { - throw new Error('Method not implemented.'); - } - - onDidEditStat: Event; - private _roots: ExplorerItem[]; + private _onDidChangeEditable = new Emitter(); private _listener: IDisposable; constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { @@ -35,16 +30,25 @@ export class ExplorerService implements IExplorerService { setRoots(); } - public get roots(): ExplorerItem[] { + get roots(): ExplorerItem[] { return this._roots; } + get onDidChangeEditable(): Event { + return this._onDidChangeEditable.event; + } + + setEditable(stat: ExplorerItem, editable: boolean): void { + this._onDidChangeEditable.fire(stat); + // TODO@isidor + } + /** * Returns an array of child stat from this stat that matches with the provided path. * Starts matching from the first root. * Will return empty array in case the FileStat does not exist. */ - public findAll(resource: URI): ExplorerItem[] { + findAll(resource: URI): ExplorerItem[] { return coalesce(this.roots.map(root => root.find(resource))); } @@ -53,7 +57,7 @@ export class ExplorerService implements IExplorerService { * In case multiple FileStat are matching the resource (same folder opened multiple times) returns the FileStat that has the closest root. * Will return null in case the FileStat does not exist. */ - public findClosest(resource: URI): ExplorerItem | null { + findClosest(resource: URI): ExplorerItem | null { const folder = this.contextService.getWorkspaceFolder(resource); if (folder) { const root = this.roots.filter(r => r.resource.toString() === folder.uri.toString()).pop(); @@ -65,7 +69,7 @@ export class ExplorerService implements IExplorerService { return null; } - public dispose(): void { + dispose(): void { this._listener = dispose(this._listener); } } @@ -101,23 +105,23 @@ export class ExplorerItem { this.isDirectoryResolved = false; } - public get isSymbolicLink(): boolean { + get isSymbolicLink(): boolean { return this._isSymbolicLink; } - public get isDirectory(): boolean { + get isDirectory(): boolean { return this._isDirectory; } - public get isReadonly(): boolean { + get isReadonly(): boolean { return this._isReadonly; } - public get isError(): boolean { + get isError(): boolean { return this._isError; } - public set isDirectory(value: boolean) { + set isDirectory(value: boolean) { if (value !== this._isDirectory) { this._isDirectory = value; if (this._isDirectory) { @@ -129,7 +133,7 @@ export class ExplorerItem { } - public get name(): string { + get name(): string { return this._name; } @@ -144,15 +148,15 @@ export class ExplorerItem { } } - public getId(): string { + getId(): string { return this.resource.toString(); } - public get isRoot(): boolean { + get isRoot(): boolean { return this === this.root; } - public static create(raw: IFileStat, root: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { + static create(raw: IFileStat, root: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { const stat = new ExplorerItem(raw.resource, root, raw.isSymbolicLink, raw.isReadonly, raw.isDirectory, raw.name, raw.mtime, raw.etag, isError); // Recursively add children if present @@ -183,7 +187,7 @@ export class ExplorerItem { * and children. The merge will only consider resolved stat elements to avoid overwriting data which * exists locally. */ - public static mergeLocalWithDisk(disk: ExplorerItem, local: ExplorerItem): void { + static mergeLocalWithDisk(disk: ExplorerItem, local: ExplorerItem): void { if (disk.resource.toString() !== local.resource.toString()) { return; // Merging only supported for stats with the same resource } @@ -242,7 +246,7 @@ export class ExplorerItem { /** * Adds a child element to this folder. */ - public addChild(child: ExplorerItem): void { + addChild(child: ExplorerItem): void { if (!this.children) { this.isDirectory = true; } @@ -256,7 +260,7 @@ export class ExplorerItem { } } - public getChild(name: string): ExplorerItem | undefined { + getChild(name: string): ExplorerItem | undefined { if (!this.children) { return undefined; } @@ -267,7 +271,7 @@ export class ExplorerItem { /** * Only use this method if you need all the children since it converts a map to an array */ - public getChildrenArray(): ExplorerItem[] | undefined { + getChildrenArray(): ExplorerItem[] | undefined { if (!this.children) { return undefined; } @@ -280,7 +284,7 @@ export class ExplorerItem { return items; } - public getChildrenCount(): number { + getChildrenCount(): number { if (!this.children) { return 0; } @@ -291,7 +295,7 @@ export class ExplorerItem { /** * Removes a child element from this folder. */ - public removeChild(child: ExplorerItem): void { + removeChild(child: ExplorerItem): void { if (this.children) { this.children.delete(this.getPlatformAwareName(child.name)); } @@ -304,7 +308,7 @@ export class ExplorerItem { /** * Moves this element under a new parent element. */ - public move(newParent: ExplorerItem, fnBetweenStates?: (callback: () => void) => void, fnDone?: () => void): void { + move(newParent: ExplorerItem, fnBetweenStates?: (callback: () => void) => void, fnDone?: () => void): void { if (!fnBetweenStates) { fnBetweenStates = (cb: () => void) => { cb(); }; } @@ -337,7 +341,7 @@ export class ExplorerItem { * Tells this stat that it was renamed. This requires changes to all children of this stat (if any) * so that the path property can be updated properly. */ - public rename(renamedStat: { name: string, mtime?: number }): void { + rename(renamedStat: { name: string, mtime?: number }): void { // Merge a subset of Properties that can change on rename this.updateName(renamedStat.name); @@ -351,7 +355,7 @@ export class ExplorerItem { * Returns a child stat from this stat that matches with the provided path. * Will return "null" in case the child does not exist. */ - public find(resource: URI): ExplorerItem | null { + find(resource: URI): ExplorerItem | null { // Return if path found // For performance reasons try to do the comparison as fast as possible if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && @@ -396,7 +400,7 @@ export class ExplorerItem { /* A helper that can be used to show a placeholder when creating a new stat */ export class NewStatPlaceholder extends ExplorerItem { - public static NAME = ''; + static readonly NAME = ''; private static ID = 0; private id: number; @@ -410,7 +414,7 @@ export class NewStatPlaceholder extends ExplorerItem { this.directoryPlaceholder = isDirectory; } - public destroy(): void { + destroy(): void { this.parent.removeChild(this); this.isDirectoryResolved = false; @@ -418,35 +422,35 @@ export class NewStatPlaceholder extends ExplorerItem { this.mtime = void 0; } - public getId(): string { + getId(): string { return `new-stat-placeholder:${this.id}:${this.parent.resource.toString()}`; } - public isDirectoryPlaceholder(): boolean { + isDirectoryPlaceholder(): boolean { return this.directoryPlaceholder; } - public addChild() { + addChild() { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - public removeChild() { + removeChild() { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - public move() { + move() { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - public rename() { + rename() { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - public find(resource: URI): ExplorerItem | null { + find(resource: URI): ExplorerItem | null { return null; } - public static addNewStatPlaceholder(parent: ExplorerItem, isDirectory: boolean): NewStatPlaceholder { + static addNewStatPlaceholder(parent: ExplorerItem, isDirectory: boolean): NewStatPlaceholder { const child = new NewStatPlaceholder(isDirectory, parent.root); // Inherit some parent properties to child diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 4ef69ff73e3..ee08e9026a7 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -44,7 +44,7 @@ export interface IExplorerView { export interface IExplorerService { _serviceBrand: any; readonly roots: ExplorerItem[]; - readonly onDidEditStat: Event; + readonly onDidChangeEditable: Event; setEditable(stat: ExplorerItem, editable: boolean): void; findClosest(resource: URI): ExplorerItem | null; From 096ffa944a304417bb344fcdc1aac56500cabf31 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 17 Dec 2018 18:18:04 +0100 Subject: [PATCH 13/65] explorer: actions do not take tree and funky arguments --- .../parts/files/common/explorerService.ts | 10 +- src/vs/workbench/parts/files/common/files.ts | 4 +- .../files/electron-browser/fileActions.ts | 422 ++++++------------ .../electron-browser/views/explorerView.ts | 21 +- .../electron-browser/views/explorerViewer.ts | 48 +- 5 files changed, 172 insertions(+), 333 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerService.ts b/src/vs/workbench/parts/files/common/explorerService.ts index bd079b58e14..bc19d18e70e 100644 --- a/src/vs/workbench/parts/files/common/explorerService.ts +++ b/src/vs/workbench/parts/files/common/explorerService.ts @@ -15,6 +15,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; import { coalesce } from 'vs/base/common/arrays'; import { IExplorerService } from 'vs/workbench/parts/files/common/files'; +import { IAction } from 'vs/base/common/actions'; export class ExplorerService implements IExplorerService { _serviceBrand: any; @@ -22,6 +23,7 @@ export class ExplorerService implements IExplorerService { private _roots: ExplorerItem[]; private _onDidChangeEditable = new Emitter(); private _listener: IDisposable; + private editableStats = new Map string, action: IAction }>(); constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders @@ -38,9 +40,13 @@ export class ExplorerService implements IExplorerService { return this._onDidChangeEditable.event; } - setEditable(stat: ExplorerItem, editable: boolean): void { + setEditable(stat: ExplorerItem, data: { validationMessage: (value: string) => string, action: IAction }): void { + this.editableStats.set(stat, data); this._onDidChangeEditable.fire(stat); - // TODO@isidor + } + + getEditableData(stat: ExplorerItem): { validationMessage: (value: string) => string, action: IAction } | undefined { + return this.editableStats.get(stat); } /** diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index ee08e9026a7..649d7dd737d 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -23,6 +23,7 @@ import { Schemas } from 'vs/base/common/network'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IAction } from 'vs/base/common/actions'; /** * Explorer viewlet id. @@ -46,7 +47,8 @@ export interface IExplorerService { readonly roots: ExplorerItem[]; readonly onDidChangeEditable: Event; - setEditable(stat: ExplorerItem, editable: boolean): void; + setEditable(stat: ExplorerItem, data: { validationMessage: (value: string) => string, action: IAction }): void; + getEditableData(stat: ExplorerItem): { validationMessage: (value: string) => string, action: IAction } | undefined; findClosest(resource: URI): ExplorerItem | null; findAll(resource: URI): ExplorerItem[]; } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index a9e107c0ab0..98590515c05 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -14,10 +14,9 @@ import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; import { Action, IAction } from 'vs/base/common/actions'; -import { MessageType, IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; -import { ITree, IHighlightEvent } from 'vs/base/parts/tree/browser/tree'; +import { IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; @@ -26,7 +25,7 @@ import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explo import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IInstantiationService, ServicesAccessor, IConstructorSignature3 } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; @@ -47,7 +46,6 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { EditableExplorerItems } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; import { NewStatPlaceholder, ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; export interface IEditableData { @@ -55,12 +53,6 @@ export interface IEditableData { validator: IInputValidator; } -export interface IFileViewletState { - getEditableData(stat: ExplorerItem): IEditableData; - setEditable(stat: ExplorerItem, editableData: IEditableData): void; - clearEditable(stat: ExplorerItem): void; -} - export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -137,20 +129,18 @@ class TriggerRenameFileAction extends BaseFileAction { public static readonly ID = 'renameFile'; - private tree: ITree; private renameAction: BaseRenameAction; constructor( - tree: ITree, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IExplorerService private explorerService: IExplorerService ) { super(TriggerRenameFileAction.ID, TRIGGER_RENAME_LABEL, fileService, notificationService, textFileService); - this.tree = tree; this.element = element; this.renameAction = instantiationService.createInstance(RenameFileAction, element); this._updateEnablement(); @@ -168,51 +158,9 @@ class TriggerRenameFileAction extends BaseFileAction { return this.renameAction.validateFileName(this.element.parent, name); } - public run(context?: any): Promise { - if (!context) { - return Promise.reject(new Error('No context provided to BaseEnableFileRenameAction.')); - } - - const viewletState = context.viewletState; - if (!viewletState) { - return Promise.reject(new Error('Invalid viewlet state provided to BaseEnableFileRenameAction.')); - } - - const stat = context.stat; - if (!stat) { - return Promise.reject(new Error('Invalid stat provided to BaseEnableFileRenameAction.')); - } - - viewletState.setEditable(stat, { - action: this.renameAction, - validator: (value) => { - const message = this.validateFileName(value); - - if (!message) { - return null; - } - - return { - content: message, - formatContent: true, - type: MessageType.ERROR - }; - } - }); - - this.tree.refresh(stat, false).then(() => { - this.tree.setHighlight(stat); - - const unbind = this.tree.onDidChangeHighlight((e: IHighlightEvent) => { - if (!e.highlight) { - viewletState.clearEditable(stat); - this.tree.refresh(stat); - unbind.dispose(); - } - }); - }); - - return void 0; + public run(): Promise { + this.explorerService.setEditable(this.element, { validationMessage: this.validateFileName, action: this.renameAction }); + return Promise.resolve(void 0); } } @@ -311,14 +259,13 @@ export class BaseNewAction extends BaseFileAction { constructor( id: string, label: string, - private tree: AsyncDataTree, - private viewletState: IFileViewletState, private isFile: boolean, private renameAction: BaseRenameAction, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @IExplorerService private explorerService: IExplorerService ) { super(id, label, fileService, notificationService, textFileService); @@ -327,23 +274,13 @@ export class BaseNewAction extends BaseFileAction { } } - public run(context?: any): Promise { + public run(): Promise { let folder = this.presetFolder; if (!folder) { - const focusedElements = this.tree.getFocus(); - const focus = focusedElements.length ? focusedElements[0] : undefined; - - if (focus) { - folder = focus.isDirectory ? focus : focus.parent; - } else { - folder = this.tree.getNode(null).element; - } + folder = this.explorerService.roots[0]; } - if (!folder) { - return Promise.reject(new Error('Invalid parent folder to create.')); - } if (folder.isReadonly) { return Promise.reject(new Error('Parent folder is readonly.')); } @@ -352,33 +289,12 @@ export class BaseNewAction extends BaseFileAction { return Promise.resolve(new Error('Parent folder is already in the process of creating a file')); } - this.tree.reveal(folder, 0.5); - return this.tree.expand(folder).then(() => { - const stat = NewStatPlaceholder.addNewStatPlaceholder(folder, !this.isFile); + const stat = NewStatPlaceholder.addNewStatPlaceholder(folder, !this.isFile); - this.renameAction.element = stat; + this.renameAction.element = stat; + this.explorerService.setEditable(stat, { action: this.renameAction, validationMessage: value => this.renameAction.validateFileName(folder, value) }); - this.viewletState.setEditable(stat, { - action: this.renameAction, - validator: (value) => { - const message = this.renameAction.validateFileName(folder, value); - - if (!message) { - return null; - } - - return { - content: message, - formatContent: true, - type: MessageType.ERROR - }; - } - }); - - return this.tree.refresh(folder).then(() => { - return this.tree.expand(folder).then(() => this.tree.reveal(stat, 0.5)); - }); - }); + return Promise.resolve(void 0); } } @@ -386,15 +302,14 @@ export class BaseNewAction extends BaseFileAction { export class NewFileAction extends BaseNewAction { constructor( - tree: AsyncDataTree, - fileViewletState: EditableExplorerItems, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IExplorerService explorerService: IExplorerService ) { - super('explorer.newFile', NEW_FILE_LABEL, tree, fileViewletState, true, instantiationService.createInstance(CreateFileAction, element), null, fileService, notificationService, textFileService); + super('explorer.newFile', NEW_FILE_LABEL, true, instantiationService.createInstance(CreateFileAction, element), null, fileService, notificationService, textFileService, explorerService); this.class = 'explorer-action new-file'; this._updateEnablement(); @@ -405,15 +320,14 @@ export class NewFileAction extends BaseNewAction { export class NewFolderAction extends BaseNewAction { constructor( - tree: AsyncDataTree, - fileViewletState: EditableExplorerItems, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IExplorerService explorerService: IExplorerService ) { - super('explorer.newFolder', NEW_FOLDER_LABEL, tree, fileViewletState, false, instantiationService.createInstance(CreateFolderAction, element), null, fileService, notificationService, textFileService); + super('explorer.newFolder', NEW_FOLDER_LABEL, false, instantiationService.createInstance(CreateFolderAction, element), null, fileService, notificationService, textFileService, explorerService); this.class = 'explorer-action new-folder'; this._updateEnablement(); @@ -510,7 +424,6 @@ class BaseDeleteFileAction extends BaseFileAction { private skipConfirm: boolean; constructor( - private tree: ITree, private elements: ExplorerItem[], private useTrash: boolean, @IFileService fileService: IFileService, @@ -521,7 +434,6 @@ class BaseDeleteFileAction extends BaseFileAction { ) { super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, fileService, notificationService, textFileService); - this.tree = tree; this.useTrash = useTrash && elements.every(e => !paths.isUNC(e.resource.fsPath)); // on UNC shares there is no trash this._updateEnablement(); @@ -533,11 +445,6 @@ class BaseDeleteFileAction extends BaseFileAction { public run(): Promise { - // Remove highlight - if (this.tree) { - this.tree.clearHighlight(); - } - let primaryButton: string; if (this.useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -634,48 +541,41 @@ class BaseDeleteFileAction extends BaseFileAction { } // Call function - const servicePromise = Promise.all(distinctElements.map(e => this.fileService.del(e.resource, { useTrash: this.useTrash, recursive: true }))).then(() => { - if (distinctElements[0].parent) { - this.tree.setFocus(distinctElements[0].parent); // move focus to parent - } - }, (error: any) => { - - // Handle error to delete file(s) from a modal confirmation dialog - let errorMessage: string; - let detailMessage: string; - let primaryButton: string; - if (this.useTrash) { - errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); - detailMessage = nls.localize('irreversible', "This action is irreversible!"); - primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); - } else { - errorMessage = toErrorMessage(error, false); - primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); - } - - return this.dialogService.confirm({ - message: errorMessage, - detail: detailMessage, - type: 'warning', - primaryButton - }).then(res => { - - // Focus back to tree - this.tree.domFocus(); - - if (res.confirmed) { - if (this.useTrash) { - this.useTrash = false; // Delete Permanently - } - - this.skipConfirm = true; - - return this.run(); + const servicePromise = Promise.all(distinctElements.map(e => this.fileService.del(e.resource, { useTrash: this.useTrash, recursive: true }))) + .then(undefined, (error: any) => { + // Handle error to delete file(s) from a modal confirmation dialog + let errorMessage: string; + let detailMessage: string; + let primaryButton: string; + if (this.useTrash) { + errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); + detailMessage = nls.localize('irreversible', "This action is irreversible!"); + primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); + } else { + errorMessage = toErrorMessage(error, false); + primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); } - return Promise.resolve(void 0); + return this.dialogService.confirm({ + message: errorMessage, + detail: detailMessage, + type: 'warning', + primaryButton + }).then(res => { + + if (res.confirmed) { + if (this.useTrash) { + this.useTrash = false; // Delete Permanently + } + + this.skipConfirm = true; + + return this.run(); + } + + return Promise.resolve(void 0); + }); }); - }); return servicePromise; }); @@ -734,21 +634,19 @@ class BaseDeleteFileAction extends BaseFileAction { /* Add File */ export class AddFilesAction extends BaseFileAction { - private tree: ITree; constructor( - tree: ITree, element: ExplorerItem, clazz: string, @IFileService fileService: IFileService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @IExplorerService private explorerService: IExplorerService ) { super('workbench.files.action.addFile', nls.localize('addFiles', "Add Files"), fileService, notificationService, textFileService); - this.tree = tree; this.element = element; if (clazz) { @@ -759,102 +657,89 @@ export class AddFilesAction extends BaseFileAction { } public run(resourcesToAdd: URI[]): Promise { - const addPromise = Promise.resolve(null).then(() => { - if (resourcesToAdd && resourcesToAdd.length > 0) { + if (resourcesToAdd && resourcesToAdd.length > 0) { - // Find parent to add to - let targetElement: ExplorerItem; - if (this.element) { - targetElement = this.element; - } else { - // TODO@isidor - // const input: ExplorerItem | Model = this.tree.getInput(); - // targetElement = this.tree.getFocus() || (input instanceof Model ? input.roots[0] : input); - } - - if (!targetElement.isDirectory) { - targetElement = targetElement.parent; - } - - // Resolve target to check for name collisions and ask user - return this.fileService.resolveFile(targetElement.resource).then((targetStat: IFileStat) => { - - // Check for name collisions - const targetNames = new Set(); - targetStat.children.forEach((child) => { - targetNames.add(isLinux ? child.name : child.name.toLowerCase()); - }); - - let overwritePromise: Promise = Promise.resolve({ confirmed: true }); - if (resourcesToAdd.some(resource => { - return targetNames.has(!resources.hasToIgnoreCase(resource) ? resources.basename(resource) : resources.basename(resource).toLowerCase()); - })) { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - overwritePromise = this.dialogService.confirm(confirm); - } - - return overwritePromise.then(res => { - if (!res.confirmed) { - return void 0; - } - - // Run add in sequence - const addPromisesFactory: ITask>[] = []; - resourcesToAdd.forEach(resource => { - addPromisesFactory.push(() => { - const sourceFile = resource; - const targetFile = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); - - // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents - // of the target file would replace the contents of the added file. since we already - // confirmed the overwrite before, this is OK. - let revertPromise: Promise = Promise.resolve(null); - if (this.textFileService.isDirty(targetFile)) { - revertPromise = this.textFileService.revertAll([targetFile], { soft: true }); - } - - return revertPromise.then(() => { - const target = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); - return this.fileService.copyFile(sourceFile, target, true).then(stat => { - - // if we only add one file, just open it directly - if (resourcesToAdd.length === 1) { - this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); - } - }, error => this.onError(error)); - }); - }); - }); - - return sequence(addPromisesFactory); - }); - }); + // Find parent to add to + let targetElement: ExplorerItem; + if (this.element) { + targetElement = this.element; + } else { + targetElement = this.explorerService.roots[0]; } - return void 0; - }); + if (!targetElement.isDirectory) { + targetElement = targetElement.parent; + } - return addPromise.then(() => { - this.tree.clearHighlight(); - }, (error: any) => { - this.onError(error); - this.tree.clearHighlight(); - }); + // Resolve target to check for name collisions and ask user + return this.fileService.resolveFile(targetElement.resource).then((targetStat: IFileStat) => { + + // Check for name collisions + const targetNames = new Set(); + targetStat.children.forEach((child) => { + targetNames.add(isLinux ? child.name : child.name.toLowerCase()); + }); + + let overwritePromise: Promise = Promise.resolve({ confirmed: true }); + if (resourcesToAdd.some(resource => { + return targetNames.has(!resources.hasToIgnoreCase(resource) ? resources.basename(resource) : resources.basename(resource).toLowerCase()); + })) { + const confirm: IConfirmation = { + message: nls.localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), + detail: nls.localize('irreversible', "This action is irreversible!"), + primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + overwritePromise = this.dialogService.confirm(confirm); + } + + return overwritePromise.then(res => { + if (!res.confirmed) { + return void 0; + } + + // Run add in sequence + const addPromisesFactory: ITask>[] = []; + resourcesToAdd.forEach(resource => { + addPromisesFactory.push(() => { + const sourceFile = resource; + const targetFile = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); + + // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents + // of the target file would replace the contents of the added file. since we already + // confirmed the overwrite before, this is OK. + let revertPromise: Promise = Promise.resolve(null); + if (this.textFileService.isDirty(targetFile)) { + revertPromise = this.textFileService.revertAll([targetFile], { soft: true }); + } + + return revertPromise.then(() => { + const target = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); + return this.fileService.copyFile(sourceFile, target, true).then(stat => { + + // if we only add one file, just open it directly + if (resourcesToAdd.length === 1) { + this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); + } + }, error => this.onError(error)); + }); + }); + }); + + return sequence(addPromisesFactory); + }); + }); + } + + return Promise.resolve(void 0); } } // Copy File/Folder class CopyFileAction extends BaseFileAction { - private tree: ITree; constructor( - tree: ITree, private elements: ExplorerItem[], @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @@ -864,7 +749,6 @@ class CopyFileAction extends BaseFileAction { ) { super('filesExplorer.copy', COPY_FILE_LABEL, fileService, notificationService, textFileService); - this.tree = tree; this._updateEnablement(); } @@ -873,13 +757,6 @@ class CopyFileAction extends BaseFileAction { // Write to clipboard as file/folder to copy this.clipboardService.writeResources(this.elements.map(e => e.resource)); - // Remove highlight - if (this.tree) { - this.tree.clearHighlight(); - } - - this.tree.domFocus(); - return Promise.resolve(null); } } @@ -889,24 +766,19 @@ class PasteFileAction extends BaseFileAction { public static readonly ID = 'filesExplorer.paste'; - private tree: ITree; - constructor( - tree: ITree, element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService, - @IEditorService private editorService: IEditorService + @IEditorService private editorService: IEditorService, + @IExplorerService private explorerService: IExplorerService ) { super(PasteFileAction.ID, PASTE_FILE_LABEL, fileService, notificationService, textFileService); - this.tree = tree; this.element = element; if (!this.element) { - // TODO@isidor - // const input: ExplorerItem | Model = this.tree.getInput(); - // this.element = input instanceof Model ? input.roots[0] : input; + this.element = this.explorerService.roots[0]; } this._updateEnablement(); } @@ -920,11 +792,6 @@ class PasteFileAction extends BaseFileAction { return this.fileService.resolveFile(fileToPaste).then(fileToPasteStat => { - // Remove highlight - if (this.tree) { - this.tree.clearHighlight(); - } - // Find target let target: ExplorerItem; if (this.element.resource.toString() === fileToPaste.toString()) { @@ -942,8 +809,6 @@ class PasteFileAction extends BaseFileAction { } return void 0; - }, error => this.onError(error)).then(() => { - this.tree.domFocus(); }); }, error => { this.onError(new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); @@ -953,11 +818,9 @@ class PasteFileAction extends BaseFileAction { // Duplicate File/Folder export class DuplicateFileAction extends BaseFileAction { - private tree: ITree; private target: ExplorerItem; constructor( - tree: ITree, fileToDuplicate: ExplorerItem, target: ExplorerItem, @IFileService fileService: IFileService, @@ -967,19 +830,12 @@ export class DuplicateFileAction extends BaseFileAction { ) { super('workbench.files.action.duplicateFile', nls.localize('duplicateFile', "Duplicate"), fileService, notificationService, textFileService); - this.tree = tree; this.element = fileToDuplicate; this.target = (target && target.isDirectory) ? target : fileToDuplicate.parent; this._updateEnablement(); } public run(): Promise { - - // Remove highlight - if (this.tree) { - this.tree.clearHighlight(); - } - // Copy File const result = this.fileService.copyFile(this.element.resource, findValidPasteFileTarget(this.target, { resource: this.element.resource, isDirectory: this.element.isDirectory })).then(stat => { if (!stat.isDirectory) { @@ -1544,7 +1400,7 @@ function getContext(listWidget: ListWidget): IExplorerContext { // TODO@isidor these commands are calling into actions due to the complex inheritance action structure. // It should be the other way around, that actions call into commands. -function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature3, EditableExplorerItems, ExplorerItem, Action>): Promise { +function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature1): Promise { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); const viewletService = accessor.get(IViewletService); @@ -1559,7 +1415,7 @@ function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: ICons if (explorerView && explorerView.isVisible() && explorerView.isExpanded()) { explorerView.focus(); const { stat } = getContext(listService.lastFocusedList); - const action = instantationService.createInstance(constructor, listService.lastFocusedList, explorerView.editableExplorerItems, stat); + const action = instantationService.createInstance(constructor, stat); return action.run(); } @@ -1587,8 +1443,8 @@ export const renameHandler = (accessor: ServicesAccessor) => { const listService = accessor.get(IListService); const explorerContext = getContext(listService.lastFocusedList); - const renameAction = instantationService.createInstance(TriggerRenameFileAction, listService.lastFocusedList, explorerContext.stat); - return renameAction.run(explorerContext); + const renameAction = instantationService.createInstance(TriggerRenameFileAction, explorerContext.stat); + return renameAction.run(); }; export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { @@ -1597,7 +1453,7 @@ export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; - const moveFileToTrashAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, true); + const moveFileToTrashAction = instantationService.createInstance(BaseDeleteFileAction, stats, true); return moveFileToTrashAction.run(); }; @@ -1607,7 +1463,7 @@ export const deleteFileHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; - const deleteFileAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, false); + const deleteFileAction = instantationService.createInstance(BaseDeleteFileAction, stats, false); return deleteFileAction.run(); }; @@ -1617,7 +1473,7 @@ export const copyFileHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; - const copyFileAction = instantationService.createInstance(CopyFileAction, listService.lastFocusedList, stats); + const copyFileAction = instantationService.createInstance(CopyFileAction, stats); return copyFileAction.run(); }; @@ -1628,7 +1484,7 @@ export const pasteFileHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); return Promise.all(resources.distinctParents(clipboardService.readResources(), r => r).map(toCopy => { - const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat); + const pasteFileAction = instantationService.createInstance(PasteFileAction, explorerContext.stat); return pasteFileAction.run(toCopy); })); }; diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 045a3083d6b..6127810b457 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -19,7 +19,6 @@ import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as DOM from 'vs/base/browser/dom'; import { CollapseAction2 } from 'vs/workbench/browser/viewlet'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { ExplorerDecorationsProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -40,7 +39,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, EditableExplorerItems as EditableExplorerItems, FilesFilter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -50,10 +49,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; -export interface IExplorerViewOptions extends IViewletViewOptions { - fileViewletState: EditableExplorerItems; -} - function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): glob.IExpression { const scope = root ? { resource: root } : void 0; const configuration = configurationService.getValue(scope); @@ -70,7 +65,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private tree: WorkbenchAsyncDataTree; private filter: FilesFilter; private isCreated: boolean; - private _editableExplorerItems: EditableExplorerItems; private explorerRefreshDelayer: ThrottledDelayer; @@ -87,7 +81,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private isDisposed = false; constructor( - options: IExplorerViewOptions, + options: IViewletPanelOptions, @INotificationService private notificationService: INotificationService, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService private instantiationService: IInstantiationService, @@ -110,7 +104,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { ) { super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); - this._editableExplorerItems = new EditableExplorerItems(); this.explorerRefreshDelayer = new ThrottledDelayer(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY); this.resourceContext = instantiationService.createInstance(ResourceContextKey); @@ -160,10 +153,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { // noop } - get editableExplorerItems(): EditableExplorerItems { - return this._editableExplorerItems; - } - // Memoized locals @memoize private get fileEventsFilter(): ResourceGlobMatcher { const fileEventsFilter = this.instantiationService.createInstance( @@ -228,8 +217,8 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { getActions(): IAction[] { const actions: Action[] = []; - actions.push(this.instantiationService.createInstance(NewFileAction, this.tree, this._editableExplorerItems, null)); - actions.push(this.instantiationService.createInstance(NewFolderAction, this.tree, this._editableExplorerItems, null)); + actions.push(this.instantiationService.createInstance(NewFileAction, null)); + actions.push(this.instantiationService.createInstance(NewFolderAction, null)); actions.push(this.instantiationService.createInstance(RefreshViewExplorerAction, this, 'explorer-action refresh-explorer')); actions.push(this.instantiationService.createInstance(CollapseAction2, this.tree, true, 'explorer-action collapse-explorer')); @@ -373,7 +362,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private createTree(container: HTMLElement): void { this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); - const filesRenderer = this.instantiationService.createInstance(FilesRenderer, this._editableExplorerItems); + const filesRenderer = this.instantiationService.createInstance(FilesRenderer); this.disposables.push(filesRenderer); this.tree = new WorkbenchAsyncDataTree(container, new ExplorerDelegate(), [filesRenderer], diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 3a6fb501de0..652faa274fa 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -16,7 +16,6 @@ import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IDataSource } from 'vs/base/browser/ui/tree/tree'; -import { IFileViewletState, IEditableData } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,6 +32,7 @@ import { rtrim } from 'vs/base/common/strings'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; +import { IAction } from 'vs/base/common/actions'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -118,45 +118,20 @@ export interface IFileTemplateData { container: HTMLElement; } -export class EditableExplorerItems implements IFileViewletState { - private editableStats: Map; - - constructor() { - this.editableStats = new Map(); - } - - public getEditableData(stat: ExplorerItem): IEditableData { - return this.editableStats.get(stat); - } - - public setEditable(stat: ExplorerItem, editableData: IEditableData): void { - if (editableData) { - this.editableStats.set(stat, editableData); - } - } - - public clearEditable(stat: ExplorerItem): void { - this.editableStats.delete(stat); - } -} - export class FilesRenderer implements ITreeRenderer, IDisposable { static readonly ID = 'file'; - private state: EditableExplorerItems; private config: IFilesConfiguration; private configListener: IDisposable; constructor( - state: EditableExplorerItems, @IContextViewService private contextViewService: IContextViewService, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService - + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IExplorerService private explorerService: IExplorerService ) { - this.state = state; this.config = this.configurationService.getValue(); this.configListener = this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('explorer')) { @@ -179,7 +154,7 @@ export class FilesRenderer implements ITreeRenderer, index: number, templateData: IFileTemplateData): void { templateData.elementDisposable.dispose(); const stat = element.element; - const editableData: IEditableData = this.state.getEditableData(stat); + const editableData = this.explorerService.getEditableData(stat); // File Label if (!editableData) { @@ -206,7 +181,7 @@ export class FilesRenderer implements ITreeRenderer string, action: IAction }): void { // Use a file label only for the icon next to the input box const label = this.instantiationService.createInstance(FileLabel, container, void 0); @@ -222,7 +197,18 @@ export class FilesRenderer implements ITreeRenderer { + const content = editableData.validationMessage(value); + if (!content) { + return null; + } + + return { + content, + formatContent: true, + type: MessageType.ERROR + }; + } }, ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.") }); From 6a48614c49c3226a4dee7e1a882f0a039fe61ec3 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 Dec 2018 11:30:43 +0100 Subject: [PATCH 14/65] explorer: adopt to tree interface changes --- .../workbench/parts/files/electron-browser/fileActions.ts | 2 +- .../parts/files/electron-browser/views/explorerView.ts | 6 +++--- .../parts/files/electron-browser/views/explorerViewer.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 98590515c05..37b83cd2abc 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -1389,7 +1389,7 @@ interface IExplorerContext { function getContext(listWidget: ListWidget): IExplorerContext { // These commands can only be triggered when explorer viewlet is visible so get it using the active viewlet - const tree = >listWidget; + const tree = >listWidget; const focus = tree.getFocus(); const stat = focus.length ? focus[0] : undefined; const selection = tree.getSelection(); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 6127810b457..64ae48d7ed6 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -62,7 +62,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes - private tree: WorkbenchAsyncDataTree; + private tree: WorkbenchAsyncDataTree; private filter: FilesFilter; private isCreated: boolean; @@ -831,7 +831,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } }); - return this.tree.refresh(null); + return this.tree.setInput(null); }); } @@ -844,7 +844,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { ExplorerItem.mergeLocalWithDisk(modelStat, this.explorerService.roots[index]); } - return this.tree.refresh(null); + return this.tree.setInput(null); }))); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 652faa274fa..0e37fb2ba4b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -15,7 +15,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -47,7 +47,7 @@ export class ExplorerDelegate implements IListVirtualDelegate { } } -export class ExplorerDataSource implements IDataSource { +export class ExplorerDataSource implements IAsyncDataSource { constructor( @IExplorerService private explorerService: IExplorerService, From 4d048e02d76f0d13d8076509b4a1121e23c94698 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 Dec 2018 11:37:27 +0100 Subject: [PATCH 15/65] explorer: properly handle mouse middle button --- .../electron-browser/views/explorerView.ts | 5 +- .../electron-browser/views/explorerViewer.ts | 139 ------------------ 2 files changed, 4 insertions(+), 140 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 64ae48d7ed6..a2e77e85eb9 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -405,8 +405,11 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (selection && selection.length === 1) { let isDoubleClick = false; let sideBySide = false; + let isMiddleClick = false; + if (e.browserEvent instanceof MouseEvent) { isDoubleClick = e.browserEvent.detail === 2; + isMiddleClick = e.browserEvent.button === 1; sideBySide = this.tree.useAltAsMultipleSelectionModifier ? (e.browserEvent.ctrlKey || e.browserEvent.metaKey) : e.browserEvent.altKey; } @@ -418,7 +421,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } }*/ this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: !isDoubleClick, pinned: isDoubleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } } })); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 0e37fb2ba4b..6a9ba0ff1ce 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -384,145 +384,6 @@ export class FilesFilter implements ITreeFilter { } } -// Explorer Controller -// export class FileController extends WorkbenchTreeController implements IDisposable { -// private fileCopiedContextKey: IContextKey; -// private contributedContextMenu: IMenu; -// private previousSelectionRangeStop: ExplorerItem; - -// constructor( -// @IEditorService private editorService: IEditorService, -// @IContextMenuService private contextMenuService: IContextMenuService, -// @ITelemetryService private telemetryService: ITelemetryService, -// @IMenuService private menuService: IMenuService, -// @IContextKeyService contextKeyService: IContextKeyService, -// @IClipboardService private clipboardService: IClipboardService, -// @IConfigurationService configurationService: IConfigurationService -// ) { -// super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); - -// this.fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService); -// } - -// public onLeftClick(tree: WorkbenchTree, stat: ExplorerItem | Model, event: IMouseEvent, origin: string = 'mouse'): boolean { -// const payload = { origin: origin }; -// const isDoubleClick = (origin === 'mouse' && event.detail === 2); - -// // Handle Highlight Mode -// if (tree.getHighlight()) { - -// // Cancel Event -// event.preventDefault(); -// event.stopPropagation(); - -// tree.clearHighlight(payload); - -// return false; -// } - -// // Handle root -// if (stat instanceof Model) { -// tree.clearFocus(payload); -// tree.clearSelection(payload); - -// return false; -// } - -// // Cancel Event -// const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; -// if (!isMouseDown) { -// event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise -// } -// event.stopPropagation(); - -// // Set DOM focus -// tree.domFocus(); -// if (stat instanceof NewStatPlaceholder) { -// return true; -// } - -// // Allow to multiselect -// if ((tree.useAltAsMultipleSelectionModifier && event.altKey) || !tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey)) { -// const selection = tree.getSelection(); -// this.previousSelectionRangeStop = undefined; -// if (selection.indexOf(stat) >= 0) { -// tree.setSelection(selection.filter(s => s !== stat)); -// } else { -// tree.setSelection(selection.concat(stat)); -// tree.setFocus(stat, payload); -// } -// } - -// // Allow to unselect -// else if (event.shiftKey) { -// const focus = tree.getFocus(); -// if (focus) { -// if (this.previousSelectionRangeStop) { -// tree.deselectRange(stat, this.previousSelectionRangeStop); -// } -// tree.selectRange(focus, stat, payload); -// this.previousSelectionRangeStop = stat; -// } -// } - -// // Select, Focus and open files -// else { - -// // Expand / Collapse -// if (isDoubleClick || this.openOnSingleClick || this.isClickOnTwistie(event)) { -// tree.toggleExpansion(stat, event.altKey); -// this.previousSelectionRangeStop = undefined; -// } - -// const preserveFocus = !isDoubleClick; -// tree.setFocus(stat, payload); - -// if (isDoubleClick) { -// event.preventDefault(); // focus moves to editor, we need to prevent default -// } - -// tree.setSelection([stat], payload); - -// if (!stat.isDirectory && (isDoubleClick || this.openOnSingleClick)) { -// let sideBySide = false; -// if (event) { -// sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; -// } - -// this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick }); -// } -// } - -// return true; -// } - -// public onMouseMiddleClick(tree: WorkbenchTree, element: ExplorerItem | Model, event: IMouseEvent): boolean { -// let sideBySide = false; -// if (event) { -// sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; -// } -// if (element instanceof ExplorerItem && !element.isDirectory) { -// this.openEditor(element, { preserveFocus: true, sideBySide, pinned: true }); -// } - -// return true; -// } - -// public openEditor(stat: ExplorerItem, options: { preserveFocus: boolean; sideBySide: boolean; pinned: boolean; }): void { -// if (stat && !stat.isDirectory) { -// /* __GDPR__ -// "workbenchActionExecuted" : { -// "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, -// "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } -// } -// */ -// this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - -// this.editorService.openEditor({ resource: stat.resource, options }, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); -// } -// } -// } - // // Explorer Sorter // export class FileSorter implements ISorter { // private toDispose: IDisposable[]; From 688595bb5c6a07ef94d587d02cf153606a1873da Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 Dec 2018 15:08:04 +0100 Subject: [PATCH 16/65] explorer: split logic between view, service and model --- src/vs/workbench/parts/files/browser/files.ts | 2 +- .../{explorerService.ts => explorerModel.ts} | 45 +-- src/vs/workbench/parts/files/common/files.ts | 13 +- .../files/electron-browser/explorerService.ts | 270 ++++++++++++++++++ .../files/electron-browser/fileActions.ts | 2 +- .../electron-browser/files.contribution.ts | 2 +- .../electron-browser/views/explorerView.ts | 261 +---------------- .../electron-browser/views/explorerViewer.ts | 2 +- ...rService.test.ts => explorerModel.test.ts} | 2 +- 9 files changed, 296 insertions(+), 303 deletions(-) rename src/vs/workbench/parts/files/common/{explorerService.ts => explorerModel.ts} (89%) create mode 100644 src/vs/workbench/parts/files/electron-browser/explorerService.ts rename src/vs/workbench/parts/files/test/electron-browser/{explorerService.test.ts => explorerModel.test.ts} (99%) diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index ac5b43d9cca..93bf737781c 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -5,12 +5,12 @@ import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; -import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; import { OpenEditor } from 'vs/workbench/parts/files/common/files'; import { toResource } from 'vs/workbench/common/editor'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; // Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding // To cover all these cases we need to properly compute the resource on which the command is being executed diff --git a/src/vs/workbench/parts/files/common/explorerService.ts b/src/vs/workbench/parts/files/common/explorerModel.ts similarity index 89% rename from src/vs/workbench/parts/files/common/explorerService.ts rename to src/vs/workbench/parts/files/common/explorerModel.ts index bc19d18e70e..b98b17a1019 100644 --- a/src/vs/workbench/parts/files/common/explorerService.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -4,28 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; import { isLinux } from 'vs/base/common/platform'; import { IFileStat } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; import { coalesce } from 'vs/base/common/arrays'; -import { IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { IAction } from 'vs/base/common/actions'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -export class ExplorerService implements IExplorerService { - _serviceBrand: any; +export class ExplorerModel implements IDisposable { private _roots: ExplorerItem[]; - private _onDidChangeEditable = new Emitter(); private _listener: IDisposable; - private editableStats = new Map string, action: IAction }>(); - constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { + constructor(private contextService: IWorkspaceContextService) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders .map(folder => new ExplorerItem(folder.uri, undefined, false, false, true, folder.name)); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => setRoots()); @@ -36,19 +30,6 @@ export class ExplorerService implements IExplorerService { return this._roots; } - get onDidChangeEditable(): Event { - return this._onDidChangeEditable.event; - } - - setEditable(stat: ExplorerItem, data: { validationMessage: (value: string) => string, action: IAction }): void { - this.editableStats.set(stat, data); - this._onDidChangeEditable.fire(stat); - } - - getEditableData(stat: ExplorerItem): { validationMessage: (value: string) => string, action: IAction } | undefined { - return this.editableStats.get(stat); - } - /** * Returns an array of child stat from this stat that matches with the provided path. * Starts matching from the first root. @@ -314,21 +295,11 @@ export class ExplorerItem { /** * Moves this element under a new parent element. */ - move(newParent: ExplorerItem, fnBetweenStates?: (callback: () => void) => void, fnDone?: () => void): void { - if (!fnBetweenStates) { - fnBetweenStates = (cb: () => void) => { cb(); }; - } - + move(newParent: ExplorerItem): void { this.parent.removeChild(this); - - fnBetweenStates(() => { - newParent.removeChild(this); // make sure to remove any previous version of the file if any - newParent.addChild(this); - this.updateResource(true); - if (fnDone) { - fnDone(); - } - }); + newParent.removeChild(this); // make sure to remove any previous version of the file if any + newParent.addChild(this); + this.updateResource(true); } private updateResource(recursive: boolean): void { diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 649d7dd737d..3f63a17f4d7 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -21,9 +21,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views'; import { Schemas } from 'vs/base/common/network'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { IAction } from 'vs/base/common/actions'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; /** * Explorer viewlet id. @@ -42,15 +42,20 @@ export interface IExplorerView { select(resource: URI, reveal?: boolean): void; } +export interface IEditableData { + validationMessage: (value: string) => string; + action: IAction; +} + export interface IExplorerService { _serviceBrand: any; readonly roots: ExplorerItem[]; + readonly onDidChangeItem: Event; readonly onDidChangeEditable: Event; - setEditable(stat: ExplorerItem, data: { validationMessage: (value: string) => string, action: IAction }): void; - getEditableData(stat: ExplorerItem): { validationMessage: (value: string) => string, action: IAction } | undefined; + setEditable(stat: ExplorerItem, data: IEditableData): void; + getEditableData(stat: ExplorerItem): IEditableData | undefined; findClosest(resource: URI): ExplorerItem | null; - findAll(resource: URI): ExplorerItem[]; } export const IExplorerService = createDecorator('explorerService'); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts new file mode 100644 index 00000000000..95dc09812f7 --- /dev/null +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -0,0 +1,270 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration } from 'vs/workbench/parts/files/common/files'; +import { ExplorerItem, ExplorerModel } from 'vs/workbench/parts/files/common/explorerModel'; +import { URI } from 'vs/base/common/uri'; +import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType } from 'vs/platform/files/common/files'; +import { dirname } from 'vs/base/common/resources'; +import { memoize } from 'vs/base/common/decorators'; +import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IExpression } from 'vs/base/common/glob'; + +function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): IExpression { + const scope = root ? { resource: root } : void 0; + const configuration = configurationService.getValue(scope); + + return (configuration && configuration.files && configuration.files.exclude) || Object.create(null); +} + +export class ExplorerService implements IExplorerService { + _serviceBrand: any; + + private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first + + private _onDidChangeItem = new Emitter(); + private _onDidChangeEditable = new Emitter(); + private disposables: IDisposable[] = []; + private editableStats = new Map(); + private sortOrder: SortOrder; + + constructor( + @IFileService private fileService: IFileService, + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { } + + get roots(): ExplorerItem[] { + return this.model.roots; + } + + get onDidChangeItem(): Event { + return this._onDidChangeItem.event; + } + + get onDidChangeEditable(): Event { + return this._onDidChangeEditable.event; + } + + // Memoized locals + @memoize private get fileEventsFilter(): ResourceGlobMatcher { + const fileEventsFilter = this.instantiationService.createInstance( + ResourceGlobMatcher, + (root: URI) => getFileEventsExcludes(this.configurationService, root), + (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) + ); + this.disposables.push(fileEventsFilter); + + return fileEventsFilter; + } + + @memoize get model(): ExplorerModel { + const model = new ExplorerModel(this.contextService); + this.disposables.push(model); + this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); + this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); + + return model; + } + + findClosest(resource: URI): ExplorerItem { + return this.model.findClosest(resource); + } + + setEditable(stat: ExplorerItem, data: IEditableData): void { + this.editableStats.set(stat, data); + this._onDidChangeEditable.fire(stat); + } + + getEditableData(stat: ExplorerItem): IEditableData | undefined { + return this.editableStats.get(stat); + } + + private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { + const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; + if (this.sortOrder !== configSortOrder) { + this.sortOrder = configSortOrder; + this.roots.forEach(r => this._onDidChangeItem.fire(r)); + } + } + + // File events + private onFileOperation(e: FileOperationEvent): void { + // Add + if (e.operation === FileOperation.CREATE || e.operation === FileOperation.COPY) { + const addedElement = e.target; + const parentResource = dirname(addedElement.resource); + const parents = this.model.findAll(parentResource); + + if (parents.length) { + + // Add the new file to its parent (Model) + parents.forEach(p => { + // We have to check if the parent is resolved #29177 + const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(null) : this.fileService.resolveFile(p.resource); + thenable.then(stat => { + if (stat) { + const modelStat = ExplorerItem.create(stat, p.root); + ExplorerItem.mergeLocalWithDisk(modelStat, p); + } + + const childElement = ExplorerItem.create(addedElement, p.root); + // Make sure to remove any previous version of the file if any + p.removeChild(childElement); + p.addChild(childElement); + // Refresh the Parent (View) + this._onDidChangeItem.fire(p); + }); + }); + } + } + + // Move (including Rename) + else if (e.operation === FileOperation.MOVE) { + const oldResource = e.resource; + const newElement = e.target; + const oldParentResource = dirname(oldResource); + const newParentResource = dirname(newElement.resource); + + // Handle Rename + if (oldParentResource && newParentResource && oldParentResource.toString() === newParentResource.toString()) { + const modelElements = this.model.findAll(oldResource); + modelElements.forEach(modelElement => { + // Rename File (Model) + modelElement.rename(newElement); + this._onDidChangeItem.fire(modelElement.parent); + }); + } + + // Handle Move + else if (oldParentResource && newParentResource) { + const newParents = this.model.findAll(newParentResource); + const modelElements = this.model.findAll(oldResource); + + if (newParents.length && modelElements.length) { + // Move in Model + modelElements.forEach((modelElement, index) => { + const oldParent = modelElement.parent; + this._onDidChangeItem.fire(oldParent); + this._onDidChangeItem.fire(newParents[index]); + }); + } + } + } + + // Delete + else if (e.operation === FileOperation.DELETE) { + 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) + this._onDidChangeItem.fire(parent); + } + }); + } + } + + private onFileChanges(e: FileChangesEvent): void { + // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) + // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might + // be fired first over the other or not at all. + setTimeout(() => { + // Filter to the ones we care + e = this.filterToViewRelevantEvents(e); + + // Handle added files/folders + const added = e.getAdded(); + if (added.length) { + + // Check added: Refresh if added file/folder is not part of resolved root and parent is part of it + const ignoredPaths: { [resource: string]: boolean } = <{ [resource: string]: boolean }>{}; + for (let i = 0; i < added.length; i++) { + const change = added[i]; + + // Find parent + const parent = dirname(change.resource); + + // Continue if parent was already determined as to be ignored + if (ignoredPaths[parent.toString()]) { + continue; + } + + // Compute if parent is visible and added file not yet part of it + const parentStat = this.model.findClosest(parent); + if (parentStat && parentStat.isDirectoryResolved && !this.model.findClosest(change.resource)) { + this._onDidChangeItem.fire(parentStat); + } + + // Keep track of path that can be ignored for faster lookup + if (!parentStat || !parentStat.isDirectoryResolved) { + ignoredPaths[parent.toString()] = true; + } + } + } + + // Handle deleted files/folders + const deleted = e.getDeleted(); + if (deleted.length) { + + // Check deleted: Refresh if deleted file/folder part of resolved root + for (let j = 0; j < deleted.length; j++) { + const del = deleted[j]; + const item = this.model.findClosest(del.resource); + if (item) { + this._onDidChangeItem.fire(item.parent); + } + } + } + + // Handle updated files/folders if we sort by modified + if (this.sortOrder === SortOrderConfiguration.MODIFIED) { + const updated = e.getUpdated(); + + // Check updated: Refresh if updated file/folder part of resolved root + for (let j = 0; j < updated.length; j++) { + const upd = updated[j]; + const item = this.model.findClosest(upd.resource); + + if (item) { + this._onDidChangeItem.fire(item.parent); + } + } + } + + }, ExplorerService.EXPLORER_FILE_CHANGES_REACT_DELAY); + } + + private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { + return new FileChangesEvent(e.changes.filter(change => { + if (change.type === FileChangeType.UPDATED && this.sortOrder !== SortOrderConfiguration.MODIFIED) { + return false; // we only are about updated if we sort by modified time + } + + if (!this.contextService.isInsideWorkspace(change.resource)) { + return false; // exclude changes for resources outside of workspace + } + + if (this.fileEventsFilter.matches(change.resource)) { + return false; // excluded via files.exclude setting + } + + return true; + })); + } + + dispose(): void { + dispose(this.disposables); + } +} diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 37b83cd2abc..4246f210ffb 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -46,7 +46,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { NewStatPlaceholder, ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; +import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; export interface IEditableData { action: IAction; diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index 451b36ca422..2399ebd3771 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -36,7 +36,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { nativeSep } from 'vs/base/common/paths'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExplorerService } from 'vs/workbench/parts/files/common/explorerService'; +import { ExplorerService } from 'vs/workbench/parts/files/electron-browser/explorerService'; // Viewlet Action export class OpenExplorerViewletAction extends ShowViewletAction { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index a2e77e85eb9..9213168c8b8 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -9,11 +9,10 @@ import * as perf from 'vs/base/common/performance'; import { ThrottledDelayer, sequence, ignoreErrors } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; -import * as glob from 'vs/base/common/glob'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileService, FILES_EXCLUDE_CONFIG, IFileStat } from 'vs/platform/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; +import { IResolveFileOptions, IFileService } from 'vs/platform/files/common/files'; import { RefreshViewExplorerAction, NewFolderAction, NewFileAction, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -29,7 +28,6 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; import { isLinux } from 'vs/base/common/platform'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService'; @@ -47,19 +45,11 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; - -function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): glob.IExpression { - const scope = root ? { resource: root } : void 0; - const configuration = configurationService.getValue(scope); - - return (configuration && configuration.files && configuration.files.exclude) || Object.create(null); -} +import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; export class ExplorerView extends ViewletPanel implements IExplorerView { static readonly ID: string = 'workbench.explorer.fileView'; - private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes private tree: WorkbenchAsyncDataTree; @@ -74,7 +64,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { private rootContext: IContextKey; private shouldRefresh: boolean; - private sortOrder: SortOrder; private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private autoReveal = false; @@ -154,17 +143,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } // Memoized locals - @memoize private get fileEventsFilter(): ResourceGlobMatcher { - const fileEventsFilter = this.instantiationService.createInstance( - ResourceGlobMatcher, - (root: URI) => getFileEventsExcludes(this.configurationService, root), - (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) - ); - this.disposables.push(fileEventsFilter); - - return fileEventsFilter; - } - @memoize private get contributedContextMenu(): IMenu { const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService); this.disposables.push(contributedContextMenu); @@ -297,7 +275,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (visible) { DOM.show(this.tree.getHTMLElement()); // If a refresh was requested and we are now visible, run it - let refreshPromise: Promise = Promise.resolve(null); + let refreshPromise = Promise.resolve(null); if (this.shouldRefresh) { refreshPromise = this.doRefresh(); this.shouldRefresh = false; // Reset flag @@ -383,9 +361,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); ExplorerFocusedContext.bindTo(this.tree.contextKeyService); - // Update Viewer based on File Change Events - this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); - this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); // Update resource context based on focused element this.disposables.push(this.tree.onDidChangeFocus(e => { @@ -451,12 +426,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { needsRefresh = this.filter.updateConfiguration(); } - const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; - if (this.sortOrder !== configSortOrder) { - this.sortOrder = configSortOrder; - needsRefresh = true; - } - if (event && !needsRefresh) { needsRefresh = event.affectsConfiguration('explorer.decorations.colors') || event.affectsConfiguration('explorer.decorations.badges'); @@ -468,228 +437,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } - private onFileOperation(e: FileOperationEvent): void { - if (!this.isCreated) { - return; // ignore if not yet created - } - - // Add - if (e.operation === FileOperation.CREATE || e.operation === FileOperation.COPY) { - const addedElement = e.target; - const parentResource = resources.dirname(addedElement.resource); - const parents = this.explorerService.findAll(parentResource); - - if (parents.length) { - - // Add the new file to its parent (Model) - parents.forEach(p => { - // We have to check if the parent is resolved #29177 - const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(null) : this.fileService.resolveFile(p.resource); - thenable.then(stat => { - if (stat) { - const modelStat = ExplorerItem.create(stat, p.root); - ExplorerItem.mergeLocalWithDisk(modelStat, p); - } - - const childElement = ExplorerItem.create(addedElement, p.root); - // Make sure to remove any previous version of the file if any - p.removeChild(childElement); - p.addChild(childElement); - // Refresh the Parent (View) - this.tree.refresh(p).then(() => { - // Reveal and focus new element - this.tree.reveal(childElement, 0.5); - this.tree.setFocus([childElement]); - }); - }); - }); - } - } - - // Move (including Rename) - else if (e.operation === FileOperation.MOVE) { - const oldResource = e.resource; - const newElement = e.target; - - const oldParentResource = resources.dirname(oldResource); - const newParentResource = resources.dirname(newElement.resource); - - // Only update focus if renamed/moved element is selected - let restoreFocus = false; - const focusedElements = this.tree.getFocus(); - const focus = focusedElements && focusedElements.length ? focusedElements[0] : undefined; - if (focus && focus.resource && focus.resource.toString() === oldResource.toString()) { - restoreFocus = true; - } - - let isExpanded = false; - // Handle Rename - if (oldParentResource && newParentResource && oldParentResource.toString() === newParentResource.toString()) { - const modelElements = this.explorerService.findAll(oldResource); - modelElements.forEach(modelElement => { - //Check if element is expanded - isExpanded = !this.tree.isCollapsed(modelElement); - // Rename File (Model) - modelElement.rename(newElement); - - // Update Parent (View) - this.tree.refresh(modelElement.parent).then(() => { - - // Select in Viewer if set - if (restoreFocus) { - this.tree.setFocus([modelElement]); - } - //Expand the element again - if (isExpanded) { - this.tree.expand(modelElement); - } - }); - }); - } - - // Handle Move - else if (oldParentResource && newParentResource) { - const newParents = this.explorerService.findAll(newParentResource); - const modelElements = this.explorerService.findAll(oldResource); - - if (newParents.length && modelElements.length) { - - // Move in Model - modelElements.forEach((modelElement, index) => { - const oldParent = modelElement.parent; - modelElement.move(newParents[index], (callback: () => void) => { - // Update old parent - this.tree.refresh(oldParent).then(callback); - }, () => { - // Update new parent - this.tree.refresh(newParents[index], true).then(() => this.tree.expand(newParents[index])); - }); - }); - } - } - } - - // Delete - else if (e.operation === FileOperation.DELETE) { - const modelElements = this.explorerService.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 = document.activeElement === this.tree.getHTMLElement(); - this.tree.refresh(parent).then(() => { - - // Ensure viewer has keyboard focus if event originates from viewer - if (restoreFocus) { - this.tree.domFocus(); - } - }); - } - }); - } - } - - private onFileChanges(e: FileChangesEvent): void { - // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) - // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might - // be fired first over the other or not at all. - setTimeout(() => { - if (!this.shouldRefresh && this.shouldRefreshFromEvent(e)) { - this.refreshFromEvent(); - } - }, ExplorerView.EXPLORER_FILE_CHANGES_REACT_DELAY); - } - - private shouldRefreshFromEvent(e: FileChangesEvent): boolean { - if (!this.isCreated) { - return false; - } - - // Filter to the ones we care - e = this.filterToViewRelevantEvents(e); - - // Handle added files/folders - const added = e.getAdded(); - if (added.length) { - - // Check added: Refresh if added file/folder is not part of resolved root and parent is part of it - const ignoredPaths: { [resource: string]: boolean } = <{ [resource: string]: boolean }>{}; - for (let i = 0; i < added.length; i++) { - const change = added[i]; - - // Find parent - const parent = resources.dirname(change.resource); - - // Continue if parent was already determined as to be ignored - if (ignoredPaths[parent.toString()]) { - continue; - } - - // Compute if parent is visible and added file not yet part of it - const parentStat = this.explorerService.findClosest(parent); - if (parentStat && parentStat.isDirectoryResolved && !this.explorerService.findClosest(change.resource)) { - return true; - } - - // Keep track of path that can be ignored for faster lookup - if (!parentStat || !parentStat.isDirectoryResolved) { - ignoredPaths[parent.toString()] = true; - } - } - } - - // Handle deleted files/folders - const deleted = e.getDeleted(); - if (deleted.length) { - - // Check deleted: Refresh if deleted file/folder part of resolved root - for (let j = 0; j < deleted.length; j++) { - const del = deleted[j]; - - if (this.explorerService.findClosest(del.resource)) { - return true; - } - } - } - - // Handle updated files/folders if we sort by modified - if (this.sortOrder === SortOrderConfiguration.MODIFIED) { - const updated = e.getUpdated(); - - // Check updated: Refresh if updated file/folder part of resolved root - for (let j = 0; j < updated.length; j++) { - const upd = updated[j]; - - if (this.explorerService.findClosest(upd.resource)) { - return true; - } - } - } - - return false; - } - - private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { - return new FileChangesEvent(e.changes.filter(change => { - if (change.type === FileChangeType.UPDATED && this.sortOrder !== SortOrderConfiguration.MODIFIED) { - return false; // we only are about updated if we sort by modified time - } - - if (!this.contextService.isInsideWorkspace(change.resource)) { - return false; // exclude changes for resources outside of workspace - } - - if (this.fileEventsFilter.matches(change.resource)) { - return false; // excluded via files.exclude setting - } - - return true; - })); - } - private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void { if (this.isVisible() && !this.isDisposed) { this.explorerRefreshDelayer.trigger(() => { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 6a9ba0ff1ce..8a95f09638c 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -31,8 +31,8 @@ import { normalize, join, nativeSep } from 'vs/base/common/paths'; import { rtrim } from 'vs/base/common/strings'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; -import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerService'; import { IAction } from 'vs/base/common/actions'; +import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; export class ExplorerDelegate implements IListVirtualDelegate { diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts similarity index 99% rename from src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts rename to src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index 3b03d9a38b2..b4133dfedd2 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerService.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -9,7 +9,7 @@ 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/electron-browser/fileActions'; -import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerService'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { return new ExplorerItem(toResource(path), undefined, false, false, isFolder, name, mtime); From 33cafe77ccf4684abc3485b7158298f9bcebba8e Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 10:23:58 +0100 Subject: [PATCH 17/65] explorerService.select --- .../files/browser/editors/textFileEditor.ts | 9 +-- src/vs/workbench/parts/files/common/files.ts | 16 ++--- .../files/electron-browser/explorerService.ts | 36 +++++++++- .../files/electron-browser/explorerViewlet.ts | 6 +- .../files/electron-browser/fileCommands.ts | 5 +- .../electron-browser/views/explorerView.ts | 69 +++---------------- 6 files changed, 62 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index ba0a6eac16e..0dc80557ad9 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -8,7 +8,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as types from 'vs/base/common/types'; import * as paths from 'vs/base/common/paths'; import { Action } from 'vs/base/common/actions'; -import { VIEWLET_ID, IExplorerViewlet, TEXT_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; @@ -54,7 +54,8 @@ export class TextFileEditor extends BaseTextEditor { @ITextFileService textFileService: ITextFileService, @IWindowsService private windowsService: IWindowsService, @IPreferencesService private preferencesService: IPreferencesService, - @IWindowService windowService: IWindowService + @IWindowService windowService: IWindowService, + @IExplorerService private explorerService: IExplorerService ) { super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService); @@ -218,8 +219,8 @@ export class TextFileEditor extends BaseTextEditor { // Best we can do is to reveal the folder in the explorer if (this.contextService.isInsideWorkspace(input.getResource())) { - this.viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { - return (viewlet as IExplorerViewlet).getExplorerView().select(input.getResource(), true); + this.viewletService.openViewlet(VIEWLET_ID, true).then(() => { + this.explorerService.select(input.getResource(), true); }); } }); diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 3f63a17f4d7..acc187e0581 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -15,7 +15,6 @@ import { Event } from 'vs/base/common/event'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IViewlet } from 'vs/workbench/common/viewlet'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views'; @@ -34,14 +33,6 @@ export const VIEWLET_ID = 'workbench.view.explorer'; */ export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); -export interface IExplorerViewlet extends IViewlet { - getExplorerView(): IExplorerView; -} - -export interface IExplorerView { - select(resource: URI, reveal?: boolean): void; -} - export interface IEditableData { validationMessage: (value: string) => string; action: IAction; @@ -52,10 +43,17 @@ export interface IExplorerService { readonly roots: ExplorerItem[]; readonly onDidChangeItem: Event; readonly onDidChangeEditable: Event; + readonly onDidSelectItem: Event<{ item: ExplorerItem, reveal: boolean }>; setEditable(stat: ExplorerItem, data: IEditableData): void; getEditableData(stat: ExplorerItem): IEditableData | undefined; findClosest(resource: URI): ExplorerItem | null; + + /** + * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to + * resolve the path from the disk in case the explorer is not yet expanded to the file yet. + */ + select(resource: URI, reveal?: boolean): void; } export const IExplorerService = createDecorator('explorerService'); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 95dc09812f7..2a2c48664c8 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -9,13 +9,14 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration } from 'vs/workbench/parts/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/parts/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; -import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType } from 'vs/platform/files/common/files'; +import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; import { dirname } from 'vs/base/common/resources'; import { memoize } from 'vs/base/common/decorators'; import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IExpression } from 'vs/base/common/glob'; +import { INotificationService } from 'vs/platform/notification/common/notification'; function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): IExpression { const scope = root ? { resource: root } : void 0; @@ -31,6 +32,7 @@ export class ExplorerService implements IExplorerService { private _onDidChangeItem = new Emitter(); private _onDidChangeEditable = new Emitter(); + private _onDidSelectItem = new Emitter<{ item: ExplorerItem, reveal: boolean }>(); private disposables: IDisposable[] = []; private editableStats = new Map(); private sortOrder: SortOrder; @@ -39,7 +41,8 @@ export class ExplorerService implements IExplorerService { @IFileService private fileService: IFileService, @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @INotificationService private notificationService: INotificationService, ) { } get roots(): ExplorerItem[] { @@ -54,6 +57,10 @@ export class ExplorerService implements IExplorerService { return this._onDidChangeEditable.event; } + get onDidSelectItem(): Event<{ item: ExplorerItem, reveal: boolean }> { + return this._onDidSelectItem.event; + } + // Memoized locals @memoize private get fileEventsFilter(): ResourceGlobMatcher { const fileEventsFilter = this.instantiationService.createInstance( @@ -89,6 +96,31 @@ export class ExplorerService implements IExplorerService { return this.editableStats.get(stat); } + select(resource: URI, reveal?: boolean): void { + const fileStat = this.findClosest(resource); + if (fileStat) { + this._onDidSelectItem.fire({ item: fileStat, reveal }); + return; + } + + // Stat needs to be resolved first and then revealed + const options: IResolveFileOptions = { resolveTo: [resource] }; + const workspaceFolder = this.contextService.getWorkspaceFolder(resource); + const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; + this.fileService.resolveFile(rootUri, options).then(stat => { + + // Convert to model + const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); + const modelStat = ExplorerItem.create(stat, root, options.resolveTo); + // Update Input with disk Stat + ExplorerItem.mergeLocalWithDisk(modelStat, root); + + // Select and Reveal + this._onDidSelectItem.fire({ item: root.find(resource), reveal }); + }, e => { this.notificationService.error(e); }); + } + + private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; if (this.sortOrder !== configSortOrder) { diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index fb16a1dea62..147803ba40b 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/explorerviewlet'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; -import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, IExplorerViewlet, VIEW_CONTAINER } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, VIEW_CONTAINER } from 'vs/workbench/parts/files/common/files'; import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; @@ -144,7 +144,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor } } -export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerViewlet { +export class ExplorerViewlet extends ViewContainerViewlet { private static readonly EXPLORER_VIEWS_STATE = 'workbench.explorer.views.state'; @@ -236,7 +236,7 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi } focus(): void { - const explorerView = this.getExplorerView(); + const explorerView = this.getView(ExplorerView.ID); if (explorerView && explorerView.isExpanded()) { explorerView.focus(); } else { diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 7d6505f3a82..410dc66cabb 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -11,7 +11,7 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExplorerFocusCondition, FileOnDiskContentProvider, VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; +import { ExplorerFocusCondition, FileOnDiskContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; @@ -463,6 +463,7 @@ CommandsRegistry.registerCommand({ handler: (accessor, resource: URI | object) => { const viewletService = accessor.get(IViewletService); const contextService = accessor.get(IWorkspaceContextService); + const explorerService = accessor.get(IExplorerService); const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); viewletService.openViewlet(VIEWLET_ID, false).then((viewlet: ExplorerViewlet) => { @@ -471,7 +472,7 @@ CommandsRegistry.registerCommand({ const explorerView = viewlet.getExplorerView(); if (explorerView) { explorerView.setExpanded(true); - explorerView.select(uri, true); + explorerService.select(uri, true); } } else { const openEditorsView = viewlet.getOpenEditorsView(); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 9213168c8b8..49d3103fa11 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -11,8 +11,8 @@ import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { IResolveFileOptions, IFileService } from 'vs/platform/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { RefreshViewExplorerAction, NewFolderAction, NewFileAction, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -33,7 +33,6 @@ import { IDecorationsService } from 'vs/workbench/services/decorations/browser/d import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -47,8 +46,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; -export class ExplorerView extends ViewletPanel implements IExplorerView { - +export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes @@ -71,7 +69,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { constructor( options: IViewletPanelOptions, - @INotificationService private notificationService: INotificationService, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @@ -190,6 +187,9 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { this._onDidChangeTitleArea.fire(); this.refreshFromEvent(); })); + + this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); + // TODO@Isidor plug in other explorer service events } getActions(): IAction[] { @@ -219,7 +219,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { const selection = this.hasSingleSelection(activeFile); if (!selection) { - this.select(activeFile); + this.explorerService.select(activeFile); } clearSelection = false; @@ -290,7 +290,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { const activeFile = this.getActiveFile(); if (activeFile) { refreshPromise.then(() => { - this.select(activeFile); + this.explorerService.select(activeFile); }); return; } @@ -508,7 +508,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { return this.doRefresh().then(() => { if (resourceToFocus) { - return this.select(resourceToFocus, true); + return this.explorerService.select(resourceToFocus, true); } return Promise.resolve(void 0); @@ -624,55 +624,6 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { } } - /** - * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to - * resolve the path from the disk in case the explorer is not yet expanded to the file yet. - */ - select(resource: URI, reveal: boolean = this.autoReveal): void { - - // Require valid path - if (!resource) { - return; - } - - // If path already selected, just reveal and return - const selection = this.hasSingleSelection(resource); - if (selection) { - if (reveal) { - this.tree.reveal(selection, 0.5); - } - - return; - } - - if (!this.isCreated) { - return; - } - - const fileStat = this.explorerService.findClosest(resource); - if (fileStat) { - this.doSelect(fileStat, reveal); - return; - } - - // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource] }; - const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - const rootUri = workspaceFolder ? workspaceFolder.uri : this.explorerService.roots[0].resource; - this.fileService.resolveFile(rootUri, options).then(stat => { - - // Convert to model - const root = this.explorerService.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); - const modelStat = ExplorerItem.create(stat, root, options.resolveTo); - // Update Input with disk Stat - ExplorerItem.mergeLocalWithDisk(modelStat, root); - - // Select and Reveal - return this.tree.refresh(root).then(() => this.doSelect(root.find(resource), reveal)); - - }, e => { this.notificationService.error(e); }); - } - private hasSingleSelection(resource: URI): ExplorerItem { const currentSelection: ExplorerItem[] = this.tree.getSelection(); return currentSelection.length === 1 && currentSelection[0].resource.toString() === resource.toString() @@ -680,7 +631,7 @@ export class ExplorerView extends ViewletPanel implements IExplorerView { : undefined; } - private doSelect(fileStat: ExplorerItem, reveal: boolean): Promise { + private onSelectItem(fileStat: ExplorerItem, reveal = this.autoReveal): Promise { if (!fileStat) { return Promise.resolve(void 0); } From 3eabca14342821d121dc6b51dae9bd4ae0427119 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 12:06:00 +0100 Subject: [PATCH 18/65] explorer: simplify explorer view, it no longer resolves stats --- src/vs/workbench/parts/files/common/files.ts | 4 +- .../files/electron-browser/explorerService.ts | 13 +- .../files/electron-browser/fileActions.ts | 11 +- .../files/electron-browser/fileCommands.ts | 3 +- .../electron-browser/views/explorerView.ts | 217 ++++-------------- .../electron-browser/views/explorerViewer.ts | 19 +- 6 files changed, 66 insertions(+), 201 deletions(-) diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index acc187e0581..45787cca779 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -41,7 +41,7 @@ export interface IEditableData { export interface IExplorerService { _serviceBrand: any; readonly roots: ExplorerItem[]; - readonly onDidChangeItem: Event; + readonly onDidChangeItem: Event; readonly onDidChangeEditable: Event; readonly onDidSelectItem: Event<{ item: ExplorerItem, reveal: boolean }>; @@ -53,7 +53,7 @@ export interface IExplorerService { * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to * resolve the path from the disk in case the explorer is not yet expanded to the file yet. */ - select(resource: URI, reveal?: boolean): void; + select(resource: URI, reveal?: boolean): Promise; } export const IExplorerService = createDecorator('explorerService'); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 2a2c48664c8..9c48a935e2c 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -30,7 +30,7 @@ export class ExplorerService implements IExplorerService { private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first - private _onDidChangeItem = new Emitter(); + private _onDidChangeItem = new Emitter(); private _onDidChangeEditable = new Emitter(); private _onDidSelectItem = new Emitter<{ item: ExplorerItem, reveal: boolean }>(); private disposables: IDisposable[] = []; @@ -49,7 +49,7 @@ export class ExplorerService implements IExplorerService { return this.model.roots; } - get onDidChangeItem(): Event { + get onDidChangeItem(): Event { return this._onDidChangeItem.event; } @@ -79,6 +79,7 @@ export class ExplorerService implements IExplorerService { this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); + this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this._onDidChangeItem.fire())); return model; } @@ -96,18 +97,18 @@ export class ExplorerService implements IExplorerService { return this.editableStats.get(stat); } - select(resource: URI, reveal?: boolean): void { + select(resource: URI, reveal?: boolean): Promise { const fileStat = this.findClosest(resource); if (fileStat) { this._onDidSelectItem.fire({ item: fileStat, reveal }); - return; + return Promise.resolve(void 0); } // Stat needs to be resolved first and then revealed const options: IResolveFileOptions = { resolveTo: [resource] }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; - this.fileService.resolveFile(rootUri, options).then(stat => { + return this.fileService.resolveFile(rootUri, options).then(stat => { // Convert to model const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); @@ -120,7 +121,6 @@ export class ExplorerService implements IExplorerService { }, e => { this.notificationService.error(e); }); } - private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; if (this.sortOrder !== configSortOrder) { @@ -130,6 +130,7 @@ export class ExplorerService implements IExplorerService { } // File events + private onFileOperation(e: FileOperationEvent): void { // Add if (e.operation === FileOperation.CREATE || e.operation === FileOperation.COPY) { diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 4246f210ffb..7764b2cbbf2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -20,7 +20,6 @@ import { VIEWLET_ID, IExplorerService } from 'vs/workbench/parts/files/common/fi import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; -import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -985,14 +984,6 @@ export class GlobalCompareResourcesAction extends Action { } } -// Refresh Explorer Viewer -export class RefreshViewExplorerAction extends Action { - - constructor(explorerView: ExplorerView, clazz: string) { - super('workbench.files.action.refreshFilesExplorer', nls.localize('refresh', "Refresh"), clazz, true, (context: any) => explorerView.refresh()); - } -} - export class ToggleAutoSaveAction extends Action { public static readonly ID = 'workbench.action.toggleAutoSave'; public static readonly LABEL = nls.localize('toggleAutoSave', "Toggle Auto Save"); @@ -1215,7 +1206,7 @@ export class RefreshExplorerView extends Action { label: string, @IViewletService private viewletService: IViewletService ) { - super(id, label); + super(id, label, 'explorer-action refresh-explorer'); } public run(): Promise { diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 410dc66cabb..a376b7cefc0 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -39,6 +39,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; +import { onUnexpectedError } from 'vs/base/common/errors'; // Commands @@ -472,7 +473,7 @@ CommandsRegistry.registerCommand({ const explorerView = viewlet.getExplorerView(); if (explorerView) { explorerView.setExpanded(true); - explorerService.select(uri, true); + explorerService.select(uri, true).then(undefined, onUnexpectedError); } } else { const openEditorsView = viewlet.getOpenEditorsView(); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 49d3103fa11..af8bac74d02 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -7,13 +7,11 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { ThrottledDelayer, sequence, ignoreErrors } from 'vs/base/common/async'; -import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { IFileService } from 'vs/platform/files/common/files'; -import { RefreshViewExplorerAction, NewFolderAction, NewFileAction, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as DOM from 'vs/base/browser/dom'; @@ -45,14 +43,14 @@ import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemAc import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes - private tree: WorkbenchAsyncDataTree; + private tree: WorkbenchAsyncDataTree; private filter: FilesFilter; - private isCreated: boolean; private explorerRefreshDelayer: ThrottledDelayer; @@ -61,7 +59,8 @@ export class ExplorerView extends ViewletPanel { private readonlyContext: IContextKey; private rootContext: IContextKey; - private shouldRefresh: boolean; + // Refresh is needed on the initial explorer open + private shouldRefresh = true; private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private autoReveal = false; @@ -74,7 +73,6 @@ export class ExplorerView extends ViewletPanel { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IProgressService private progressService: IProgressService, @IEditorService private editorService: IEditorService, - @IFileService private fileService: IFileService, @IPartService private partService: IPartService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService private contextKeyService: IContextKeyService, @@ -159,17 +157,11 @@ export class ExplorerView extends ViewletPanel { const configuration = this.configurationService.getValue(); this.onConfigurationUpdated(configuration); - // Load and Fill Viewer - this.doRefresh().then(() => { + // When the explorer viewer is loaded, listen to changes to the editor input + this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.revealActiveFile())); - // When the explorer viewer is loaded, listen to changes to the editor input - this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.revealActiveFile())); - - // Also handle configuration updates - this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); - - this.revealActiveFile(); - }); + // Also handle configuration updates + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); } renderBody(container: HTMLElement): void { @@ -180,16 +172,16 @@ export class ExplorerView extends ViewletPanel { this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); } - this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.refreshFromEvent(e.added))); - this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.refreshFromEvent())); - this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this.refreshFromEvent())); + this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.setTreeInput(e.added))); + this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.setTreeInput())); this.disposables.push(this.labelService.onDidRegisterFormatter(() => { this._onDidChangeTitleArea.fire(); this.refreshFromEvent(); })); + this.disposables.push(this.explorerService.onDidChangeItem(e => this.refreshFromEvent(e))); + this.disposables.push(this.explorerService.onDidChangeEditable(e => this.refresh(e.parent))); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); - // TODO@Isidor plug in other explorer service events } getActions(): IAction[] { @@ -197,7 +189,7 @@ export class ExplorerView extends ViewletPanel { actions.push(this.instantiationService.createInstance(NewFileAction, null)); actions.push(this.instantiationService.createInstance(NewFolderAction, null)); - actions.push(this.instantiationService.createInstance(RefreshViewExplorerAction, this, 'explorer-action refresh-explorer')); + actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL)); actions.push(this.instantiationService.createInstance(CollapseAction2, this.tree, true, 'explorer-action collapse-explorer')); return actions; @@ -219,7 +211,7 @@ export class ExplorerView extends ViewletPanel { if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { const selection = this.hasSingleSelection(activeFile); if (!selection) { - this.explorerService.select(activeFile); + this.explorerService.select(activeFile).then(undefined, onUnexpectedError); } clearSelection = false; @@ -270,50 +262,12 @@ export class ExplorerView extends ViewletPanel { setVisible(visible: boolean): void { super.setVisible(visible); - - // Show if (visible) { - DOM.show(this.tree.getHTMLElement()); // If a refresh was requested and we are now visible, run it - let refreshPromise = Promise.resolve(null); if (this.shouldRefresh) { - refreshPromise = this.doRefresh(); - this.shouldRefresh = false; // Reset flag + this.shouldRefresh = false; + this.setTreeInput().then(undefined, onUnexpectedError); } - - if (!this.autoReveal) { - return; // do not react to setVisible call if autoReveal === false - } - - // Always select the current navigated file in explorer if input is file editor input - // unless autoReveal is set to false - const activeFile = this.getActiveFile(); - if (activeFile) { - refreshPromise.then(() => { - this.explorerService.select(activeFile); - }); - return; - } - - // Return now if the workbench has not yet been restored - in this case the workbench takes care of restoring last used editors - if (!this.partService.isRestored()) { - return; - } - - // Otherwise restore last used file: By lastActiveFileResource - const focusedElements = this.tree.getFocus(); - if (focusedElements && focusedElements.length) { - this.editorService.openEditor({ resource: focusedElements[0].resource, options: { revealIfVisible: true } }); - return; - } - - // Otherwise restore last used file: By Explorer selection - refreshPromise.then(() => { - this.openFocusedElement(); - }); - } else { - // make sure the tree goes out of the tabindex world by hiding it - DOM.hide(this.tree.getHTMLElement()); } } @@ -361,7 +315,6 @@ export class ExplorerView extends ViewletPanel { FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); ExplorerFocusedContext.bindTo(this.tree.contextKeyService); - // Update resource context based on focused element this.disposables.push(this.tree.onDidChangeFocus(e => { const stat = e.elements && e.elements.length ? e.elements[0] : undefined; @@ -433,20 +386,14 @@ export class ExplorerView extends ViewletPanel { // Refresh viewer as needed if this originates from a config event if (event && needsRefresh) { - this.doRefresh(); + this.refresh(); } } - private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void { + private refreshFromEvent(explorerItem?: ExplorerItem): void { if (this.isVisible() && !this.isDisposed) { this.explorerRefreshDelayer.trigger(() => { - return this.doRefresh().then(() => { - if (newRoots.length === 1) { - return this.tree.reveal(this.explorerService.findClosest(newRoots[0].uri), 0.5); - } - - return undefined; - }); + return this.refresh(explorerItem); }); } else { this.shouldRefresh = true; @@ -486,118 +433,50 @@ export class ExplorerView extends ViewletPanel { /** * Refresh the contents of the explorer to get up to date data from the disk about the file structure. */ - refresh(): Promise { + refresh(item?: ExplorerItem): Promise { if (!this.tree) { return Promise.resolve(void 0); } + const toRefresh = item || this.tree.getInput(); - // Focus - this.tree.domFocus(); - - // Find resource to focus from active editor input if set - let resourceToFocus: URI; - if (this.autoReveal) { - resourceToFocus = this.getActiveFile(); - if (!resourceToFocus) { - const selection = this.tree.getSelection(); - if (selection && selection.length === 1) { - resourceToFocus = selection[0].resource; - } - } - } - - return this.doRefresh().then(() => { - if (resourceToFocus) { - return this.explorerService.select(resourceToFocus, true); - } + return this.tree.refresh(toRefresh, true); + } + private setTreeInput(newRoots?: IWorkspaceFolder[]): Promise { + if (!this.isVisible()) { + this.shouldRefresh = true; return Promise.resolve(void 0); - }); - } + } - private doRefresh(): Promise { - const targetsToResolve = this.explorerService.roots.map(root => ({ root, resource: root.resource, options: { resolveTo: [] } })); + perf.mark('willResolveExplorer'); + const roots = this.explorerService.roots; + let input: ExplorerItem | ExplorerItem[] = roots[0]; + if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) { + // Display roots only when multi folder workspace + input = roots; + } - // First time refresh: Receive target through active editor input or selection and also include settings from previous session - if (!this.isCreated) { - const activeFile = this.getActiveFile(); - if (activeFile) { - const workspaceFolder = this.contextService.getWorkspaceFolder(activeFile); - if (workspaceFolder) { - const found = targetsToResolve.filter(t => t.root.resource.toString() === workspaceFolder.uri.toString()).pop(); - found.options.resolveTo.push(activeFile); + const promise = this.tree.setInput(input).then(() => { + let expandPromise = Promise.resolve(void 0); + if (newRoots && newRoots.length) { + expandPromise = Promise.all(newRoots.map(workspaceFolder => this.tree.expand(this.explorerService.findClosest(workspaceFolder.uri)))); + } + + // Find resource to focus from active editor input if set + if (this.autoReveal) { + const resourceToFocus = this.getActiveFile(); + if (resourceToFocus) { + return expandPromise.then(() => this.explorerService.select(resourceToFocus, true)); } } - } - // Subsequent refresh: Receive targets through expanded folders in tree - else { - targetsToResolve.forEach(t => { - this.getResolvedDirectories(t.root, t.options.resolveTo); - }); - } + return expandPromise; + }).then(() => perf.mark('didResolveExplorer')); - const promise = this.resolveRoots(targetsToResolve).then(result => { - this.isCreated = true; - this.decorationProvider.changed(targetsToResolve.map(t => t.root.resource)); - return result; - }); this.progressService.showWhile(promise, this.partService.isRestored() ? 800 : 1200 /* less ugly initial startup */); - return promise; } - private resolveRoots(targetsToResolve: { root: ExplorerItem, resource: URI, options: { resolveTo: any[] } }[]): Promise { - - if (!this.isCreated) { - perf.mark('willResolveExplorer'); - } - - const errorRoot = (resource: URI, root: ExplorerItem) => { - return ExplorerItem.create({ - resource: resource, - name: paths.basename(resource.fsPath), - mtime: 0, - etag: undefined, - isDirectory: true - }, root, undefined, true); - }; - - if (targetsToResolve.every(t => t.root.resource.scheme === 'file')) { - // All the roots are local, resolve them in parallel - return this.fileService.resolveFiles(targetsToResolve).then(results => { - // Convert to model - const modelStats = results.map((result, index) => { - if (result.success && result.stat.isDirectory) { - return ExplorerItem.create(result.stat, targetsToResolve[index].root, targetsToResolve[index].options.resolveTo); - } - - return errorRoot(targetsToResolve[index].resource, targetsToResolve[index].root); - }); - // Subsequent refresh: Merge stat into our local model and refresh tree - modelStats.forEach((modelStat, index) => { - if (index < this.explorerService.roots.length) { - ExplorerItem.mergeLocalWithDisk(modelStat, this.explorerService.roots[index]); - } - }); - - return this.tree.setInput(null); - }); - } - - // There is a remote root, resolve the roots sequantally - return Promise.all(targetsToResolve.map((target, index) => this.fileService.resolveFile(target.resource, target.options) - .then(result => result.isDirectory ? ExplorerItem.create(result, target.root, target.options.resolveTo) : errorRoot(target.resource, target.root), () => errorRoot(target.resource, target.root)) - .then(modelStat => { - // Subsequent refresh: Merge stat into our local model and refresh tree - if (index < this.explorerService.roots.length) { - ExplorerItem.mergeLocalWithDisk(modelStat, this.explorerService.roots[index]); - } - - return this.tree.setInput(null); - }))); - } - /** * Given a stat, fills an array of path that make all folders below the stat that are resolved directories. */ diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 8a95f09638c..e05f559fccf 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -47,29 +47,22 @@ export class ExplorerDelegate implements IListVirtualDelegate { } } -export class ExplorerDataSource implements IAsyncDataSource { +export class ExplorerDataSource implements IAsyncDataSource { constructor( - @IExplorerService private explorerService: IExplorerService, @IProgressService private progressService: IProgressService, @INotificationService private notificationService: INotificationService, @IFileService private fileService: IFileService, @IPartService private partService: IPartService, - @IWorkspaceContextService private contextService: IWorkspaceContextService ) { } - hasChildren(element: ExplorerItem | null): boolean { - return element === null || element.isDirectory; + hasChildren(element: ExplorerItem | ExplorerItem[]): boolean { + return Array.isArray(element) || element.isDirectory; } - getChildren(element: ExplorerItem | null): Promise { - if (element === null) { - const roots = this.explorerService.roots; - if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) { - // Display roots only when multi folder workspace - return Promise.resolve(roots); - } - element = roots[0]; + getChildren(element: ExplorerItem | ExplorerItem[]): Promise { + if (Array.isArray(element)) { + return Promise.resolve(element); } // Return early if stat is already resolved From 2a2616b149f6b29e7e2ac73ad499aaa558b0e16d Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 12:20:10 +0100 Subject: [PATCH 19/65] explorer: remove unnnecessery methods --- .../electron-browser/views/explorerView.ts | 179 +++--------------- 1 file changed, 30 insertions(+), 149 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index af8bac74d02..12781a7a96d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { ThrottledDelayer, sequence, ignoreErrors } from 'vs/base/common/async'; -import * as resources from 'vs/base/common/resources'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; @@ -26,11 +25,9 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { isLinux } from 'vs/base/common/platform'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; -import { Schemas } from 'vs/base/common/network'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -64,7 +61,6 @@ export class ExplorerView extends ViewletPanel { private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private autoReveal = false; - private isDisposed = false; constructor( options: IViewletPanelOptions, @@ -102,29 +98,6 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.resourceContext); } - protected renderHeader(container: HTMLElement): void { - super.renderHeader(container); - - // Expand on drag over - this.dragHandler = new DelayedDragHandler(container, () => this.setExpanded(true)); - - const titleElement = container.querySelector('.title') as HTMLElement; - const setHeader = () => { - const workspace = this.contextService.getWorkspace(); - const title = workspace.folders.map(folder => folder.name).join(); - titleElement.textContent = this.name; - titleElement.title = title; - }; - - this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader)); - this.disposables.push(this.labelService.onDidRegisterFormatter(setHeader)); - setHeader(); - } - - protected layoutBody(size: number): void { - this.tree.layout(size); - } - get name(): string { return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace()); } @@ -150,18 +123,27 @@ export class ExplorerView extends ViewletPanel { // Split view methods - render(): void { - super.render(); + protected renderHeader(container: HTMLElement): void { + super.renderHeader(container); - // Update configuration - const configuration = this.configurationService.getValue(); - this.onConfigurationUpdated(configuration); + // Expand on drag over + this.dragHandler = new DelayedDragHandler(container, () => this.setExpanded(true)); - // When the explorer viewer is loaded, listen to changes to the editor input - this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.revealActiveFile())); + const titleElement = container.querySelector('.title') as HTMLElement; + const setHeader = () => { + const workspace = this.contextService.getWorkspace(); + const title = workspace.folders.map(folder => folder.name).join(); + titleElement.textContent = this.name; + titleElement.title = title; + }; - // Also handle configuration updates - this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); + this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader)); + this.disposables.push(this.labelService.onDidRegisterFormatter(setHeader)); + setHeader(); + } + + protected layoutBody(size: number): void { + this.tree.layout(size); } renderBody(container: HTMLElement): void { @@ -182,6 +164,16 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.explorerService.onDidChangeItem(e => this.refreshFromEvent(e))); this.disposables.push(this.explorerService.onDidChangeEditable(e => this.refresh(e.parent))); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); + + // Update configuration + const configuration = this.configurationService.getValue(); + this.onConfigurationUpdated(configuration); + + // When the explorer viewer is loaded, listen to changes to the editor input + this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.explorerService.select(this.getActiveFile()))); + + // Also handle configuration updates + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); } getActions(): IAction[] { @@ -195,71 +187,6 @@ export class ExplorerView extends ViewletPanel { return actions; } - private revealActiveFile(): void { - if (!this.autoReveal) { - return; // do not touch selection or focus if autoReveal === false - } - - let clearSelection = true; - let clearFocus = false; - - // Handle files - const activeFile = this.getActiveFile(); - if (activeFile) { - - // Select file if input is inside workspace - if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { - const selection = this.hasSingleSelection(activeFile); - if (!selection) { - this.explorerService.select(activeFile).then(undefined, onUnexpectedError); - } - - clearSelection = false; - } - } - - // Handle closed or untitled file (convince explorer to not reopen any file when getting visible) - const activeInput = this.editorService.activeEditor; - if (!activeInput || toResource(activeInput, { supportSideBySide: true, filter: Schemas.untitled })) { - clearFocus = true; - } - - // Otherwise clear - if (clearSelection) { - this.tree.setSelection([]); - } - - if (clearFocus) { - this.tree.setFocus([]); - } - } - - focus(): void { - super.focus(); - - let keepFocus = false; - - // Make sure the current selected element is revealed - if (this.tree) { - if (this.autoReveal) { - const selection = this.tree.getSelection(); - if (selection.length > 0) { - this.tree.reveal(selection[0], 0.5); - } - } - - // Pass Focus to Viewer - this.tree.domFocus(); - keepFocus = true; - } - - // Open the focused element in the editor if there is currently no file opened - const activeFile = this.getActiveFile(); - if (!activeFile) { - this.openFocusedElement(keepFocus); - } - } - setVisible(visible: boolean): void { super.setVisible(visible); if (visible) { @@ -271,14 +198,6 @@ export class ExplorerView extends ViewletPanel { } } - private openFocusedElement(preserveFocus?: boolean): void { - const focusedElements = this.tree.getFocus(); - const stat = focusedElements && focusedElements.length ? focusedElements[0] : undefined; - if (stat && !stat.isDirectory) { - this.editorService.openEditor({ resource: stat.resource, options: { preserveFocus, revealIfVisible: true } }); - } - } - private getActiveFile(): URI { const input = this.editorService.activeEditor; @@ -367,10 +286,6 @@ export class ExplorerView extends ViewletPanel { // React on events private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { - if (this.isDisposed) { - return; // guard against possible race condition when config change causes recreate of views - } - this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal; // Push down config updates to components of viewer @@ -391,7 +306,7 @@ export class ExplorerView extends ViewletPanel { } private refreshFromEvent(explorerItem?: ExplorerItem): void { - if (this.isVisible() && !this.isDisposed) { + if (this.isVisible()) { this.explorerRefreshDelayer.trigger(() => { return this.refresh(explorerItem); }); @@ -477,41 +392,8 @@ export class ExplorerView extends ViewletPanel { return promise; } - /** - * Given a stat, fills an array of path that make all folders below the stat that are resolved directories. - */ - private getResolvedDirectories(stat: ExplorerItem, resolvedDirectories: URI[]): void { - if (stat.isDirectoryResolved) { - if (!stat.isRoot) { - - // Drop those path which are parents of the current one - for (let i = resolvedDirectories.length - 1; i >= 0; i--) { - const resource = resolvedDirectories[i]; - if (resources.isEqualOrParent(stat.resource, resource, !isLinux /* ignorecase */)) { - resolvedDirectories.splice(i); - } - } - - // Add to the list of path to resolve - resolvedDirectories.push(stat.resource); - } - - // Recurse into children - stat.getChildrenArray().forEach(child => { - this.getResolvedDirectories(child, resolvedDirectories); - }); - } - } - - private hasSingleSelection(resource: URI): ExplorerItem { - const currentSelection: ExplorerItem[] = this.tree.getSelection(); - return currentSelection.length === 1 && currentSelection[0].resource.toString() === resource.toString() - ? currentSelection[0] - : undefined; - } - private onSelectItem(fileStat: ExplorerItem, reveal = this.autoReveal): Promise { - if (!fileStat) { + if (!fileStat || !this.isVisible()) { return Promise.resolve(void 0); } @@ -541,7 +423,6 @@ export class ExplorerView extends ViewletPanel { } dispose(): void { - this.isDisposed = true; if (this.dragHandler) { this.dragHandler.dispose(); } From 5c3daa440c5a38499e43f9450652dd646a72b97f Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 12:21:53 +0100 Subject: [PATCH 20/65] use undefined in explorer, not null --- .../parts/files/common/explorerModel.ts | 20 +++++++++---------- .../files/electron-browser/explorerService.ts | 2 +- .../electron-browser/views/explorerView.ts | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index b98b17a1019..36e4c2a12d7 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -42,9 +42,9 @@ export class ExplorerModel implements IDisposable { /** * Returns a FileStat that matches the passed resource. * In case multiple FileStat are matching the resource (same folder opened multiple times) returns the FileStat that has the closest root. - * Will return null in case the FileStat does not exist. + * Will return undefined in case the FileStat does not exist. */ - findClosest(resource: URI): ExplorerItem | null { + findClosest(resource: URI): ExplorerItem | undefined { const folder = this.contextService.getWorkspaceFolder(resource); if (folder) { const root = this.roots.filter(r => r.resource.toString() === folder.uri.toString()).pop(); @@ -53,7 +53,7 @@ export class ExplorerModel implements IDisposable { } } - return null; + return undefined; } dispose(): void { @@ -330,9 +330,9 @@ export class ExplorerItem { /** * Returns a child stat from this stat that matches with the provided path. - * Will return "null" in case the child does not exist. + * Will return "undefined" in case the child does not exist. */ - find(resource: URI): ExplorerItem | null { + find(resource: URI): ExplorerItem | undefined { // Return if path found // For performance reasons try to do the comparison as fast as possible if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && @@ -340,10 +340,10 @@ export class ExplorerItem { return this.findByPath(rtrim(resource.path, paths.sep), this.resource.path.length); } - return null; //Unable to find + return undefined; //Unable to find } - private findByPath(path: string, index: number): ExplorerItem | null { + private findByPath(path: string, index: number): ExplorerItem | undefined { if (paths.isEqual(rtrim(this.resource.path, paths.sep), path, !isLinux)) { return this; } @@ -370,7 +370,7 @@ export class ExplorerItem { } } - return null; + return undefined; } } @@ -423,8 +423,8 @@ export class NewStatPlaceholder extends ExplorerItem { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - find(resource: URI): ExplorerItem | null { - return null; + find(resource: URI): ExplorerItem | undefined { + return undefined; } static addNewStatPlaceholder(parent: ExplorerItem, isDirectory: boolean): NewStatPlaceholder { diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 9c48a935e2c..69fa9cf4aee 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -143,7 +143,7 @@ export class ExplorerService implements IExplorerService { // Add the new file to its parent (Model) parents.forEach(p => { // We have to check if the parent is resolved #29177 - const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(null) : this.fileService.resolveFile(p.resource); + const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(undefined) : this.fileService.resolveFile(p.resource); thenable.then(stat => { if (stat) { const modelStat = ExplorerItem.create(stat, p.root); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 12781a7a96d..1e29f06b1aa 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -179,8 +179,8 @@ export class ExplorerView extends ViewletPanel { getActions(): IAction[] { const actions: Action[] = []; - actions.push(this.instantiationService.createInstance(NewFileAction, null)); - actions.push(this.instantiationService.createInstance(NewFolderAction, null)); + actions.push(this.instantiationService.createInstance(NewFileAction, undefined)); + actions.push(this.instantiationService.createInstance(NewFolderAction, undefined)); actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL)); actions.push(this.instantiationService.createInstance(CollapseAction2, this.tree, true, 'explorer-action collapse-explorer')); @@ -203,7 +203,7 @@ export class ExplorerView extends ViewletPanel { // ignore diff editor inputs (helps to get out of diffing when returning to explorer) if (input instanceof DiffEditorInput) { - return null; + return undefined; } // check for files From 2e271d6c4292e893511b8ad110c1f7f3de100da1 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 12:32:53 +0100 Subject: [PATCH 21/65] ExplorerItem cleanup contructor --- .../parts/files/common/explorerModel.ts | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 36e4c2a12d7..21bf9b56815 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -62,50 +62,49 @@ export class ExplorerModel implements IDisposable { } export class ExplorerItem { - public resource: URI; - private _name: string; - public mtime?: number; - public etag?: string; - private _isDirectory: boolean; - private _isSymbolicLink: boolean; - private _isReadonly: boolean; private children?: Map; - private _isError: boolean; public parent: ExplorerItem; - public isDirectoryResolved: boolean; - constructor(resource: URI, public root: ExplorerItem | undefined, isSymbolicLink?: boolean, isReadonly?: boolean, isDirectory?: boolean, name: string = resources.basenameOrAuthority(resource), mtime?: number, etag?: string, isError?: boolean) { - this.resource = resource; - this._name = name; - this.isDirectory = !!isDirectory; - this._isSymbolicLink = !!isSymbolicLink; - this._isReadonly = !!isReadonly; - this.etag = etag; - this.mtime = mtime; - this._isError = !!isError; - + constructor( + public resource: URI, + public root: ExplorerItem | undefined, + private _isSymbolicLink?: boolean, + private _isReadonly?: boolean, + private _isDirectory?: boolean, + private _name: string = resources.basenameOrAuthority(resource), + private _mtime?: number, + private _etag?: string, + private _isError?: boolean + ) { if (!this.root) { this.root = this; } - this.isDirectoryResolved = false; } get isSymbolicLink(): boolean { - return this._isSymbolicLink; + return !!this._isSymbolicLink; } get isDirectory(): boolean { - return this._isDirectory; + return !!this._isDirectory; } get isReadonly(): boolean { - return this._isReadonly; + return !!this._isReadonly; + } + + get etag(): string { + return this._etag; + } + + get mtime(): number { + return this._mtime; } get isError(): boolean { - return this._isError; + return !!this._isError; } set isDirectory(value: boolean) { @@ -189,7 +188,7 @@ export class ExplorerItem { local.resource = disk.resource; local.updateName(disk.name); local.isDirectory = disk.isDirectory; - local.mtime = disk.mtime; + local._mtime = disk.mtime; local.isDirectoryResolved = disk.isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; local._isReadonly = disk.isReadonly; @@ -322,7 +321,7 @@ export class ExplorerItem { // Merge a subset of Properties that can change on rename this.updateName(renamedStat.name); - this.mtime = renamedStat.mtime; + this._mtime = renamedStat.mtime; // Update Paths including children this.updateResource(true); @@ -396,7 +395,6 @@ export class NewStatPlaceholder extends ExplorerItem { this.isDirectoryResolved = false; this.isDirectory = false; - this.mtime = void 0; } getId(): string { From 287f029be104c44c54536d8e0fdc5ed5c85cfdf4 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 12:47:17 +0100 Subject: [PATCH 22/65] explroer: move resolving of stats to model, not viewer --- .../parts/files/common/explorerModel.ts | 29 ++++++------ .../electron-browser/views/explorerViewer.ts | 46 ++++--------------- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 21bf9b56815..c6a783f9911 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -8,7 +8,7 @@ import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; import { isLinux } from 'vs/base/common/platform'; -import { IFileStat } from 'vs/platform/files/common/files'; +import { IFileStat, IFileService } from 'vs/platform/files/common/files'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -116,7 +116,6 @@ export class ExplorerItem { this.children = undefined; } } - } get name(): string { @@ -254,20 +253,24 @@ export class ExplorerItem { return this.children.get(this.getPlatformAwareName(name)); } - /** - * Only use this method if you need all the children since it converts a map to an array - */ - getChildrenArray(): ExplorerItem[] | undefined { - if (!this.children) { - return undefined; + fetchChildren(fileService: IFileService): Promise { + let promise = Promise.resolve(undefined); + if (!this.isDirectoryResolved) { + promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { + const resolved = ExplorerItem.create(stat, this.root); + ExplorerItem.mergeLocalWithDisk(resolved, this); + this.isDirectoryResolved = true; + }); } - const items: ExplorerItem[] = []; - this.children.forEach(child => { - items.push(child); - }); + return promise.then(() => { + const items: ExplorerItem[] = []; + this.children.forEach(child => { + items.push(child); + }); - return items; + return items; + }); } getChildrenCount(): number { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index e05f559fccf..4ee028de2e6 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -52,8 +52,8 @@ export class ExplorerDataSource implements IAsyncDataSource { + // Do not show error for roots since we already use an explorer decoration to notify user + if (!(element instanceof ExplorerItem && element.isRoot)) { + this.notificationService.error(e); + } - // Resolve children and add to fileStat for future lookup - else { - // Resolve - const promise = this.fileService.resolveFile(element.resource, { resolveSingleChildDescendants: true }).then(dirStat => { + return []; // we could not resolve any children because of an error + }); - // Convert to view model - const modelDirStat = ExplorerItem.create(dirStat, element.root); - - // Add children to folder - const children = modelDirStat.getChildrenArray(); - if (children) { - children.forEach(child => { - element.addChild(child); - }); - } - - element.isDirectoryResolved = true; - - return element.getChildrenArray(); - }, (e: any) => { - // Do not show error for roots since we already use an explorer decoration to notify user - if (!(element instanceof ExplorerItem && element.isRoot)) { - this.notificationService.error(e); - } - - return []; // we could not resolve any children because of an error - }); - - this.progressService.showWhile(promise, this.partService.isRestored() ? 800 : 3200 /* less ugly initial startup */); - - return promise; - } + this.progressService.showWhile(promise, this.partService.isRestored() ? 800 : 3200 /* less ugly initial startup */); + return promise; } } From b82a515257153b2df7c4b677a6e4efd7312f8855 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 13:02:18 +0100 Subject: [PATCH 23/65] explorerModel: some cleanup and fix tests --- .../parts/files/common/explorerModel.ts | 89 ++++++------------- .../electron-browser/explorerModel.test.ts | 17 +--- 2 files changed, 32 insertions(+), 74 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index c6a783f9911..83ce59db95d 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -13,6 +13,7 @@ import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/ba import { coalesce } from 'vs/base/common/arrays'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { memoize } from 'vs/base/common/decorators'; export class ExplorerModel implements IDisposable { @@ -62,7 +63,6 @@ export class ExplorerModel implements IDisposable { } export class ExplorerItem { - private children?: Map; public parent: ExplorerItem; public isDirectoryResolved: boolean; @@ -107,21 +107,14 @@ export class ExplorerItem { return !!this._isError; } - set isDirectory(value: boolean) { - if (value !== this._isDirectory) { - this._isDirectory = value; - if (this._isDirectory) { - this.children = new Map(); - } else { - this.children = undefined; - } - } - } - get name(): string { return this._name; } + @memoize get children(): Map { + return new Map(); + } + private updateName(value: string): void { // Re-add to parent since the parent has a name map to children and the name might have changed if (this.parent) { @@ -186,7 +179,7 @@ export class ExplorerItem { // Properties local.resource = disk.resource; local.updateName(disk.name); - local.isDirectory = disk.isDirectory; + local._isDirectory = disk.isDirectory; local._mtime = disk.mtime; local.isDirectoryResolved = disk.isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; @@ -198,33 +191,29 @@ export class ExplorerItem { // Map resource => stat const oldLocalChildren = new ResourceMap(); - if (local.children) { - local.children.forEach(child => { - oldLocalChildren.set(child.resource, child); - }); - } + local.children.forEach(child => { + oldLocalChildren.set(child.resource, child); + }); // Clear current children - local.children = new Map(); + local.children.clear(); // Merge received children - if (disk.children) { - disk.children.forEach(diskChild => { - const formerLocalChild = oldLocalChildren.get(diskChild.resource); - // Existing child: merge - if (formerLocalChild) { - ExplorerItem.mergeLocalWithDisk(diskChild, formerLocalChild); - formerLocalChild.parent = local; - local.addChild(formerLocalChild); - } + disk.children.forEach(diskChild => { + const formerLocalChild = oldLocalChildren.get(diskChild.resource); + // Existing child: merge + if (formerLocalChild) { + ExplorerItem.mergeLocalWithDisk(diskChild, formerLocalChild); + formerLocalChild.parent = local; + local.addChild(formerLocalChild); + } - // New child: add - else { - diskChild.parent = local; - local.addChild(diskChild); - } - }); - } + // New child: add + else { + diskChild.parent = local; + local.addChild(diskChild); + } + }); } } @@ -232,24 +221,13 @@ export class ExplorerItem { * Adds a child element to this folder. */ addChild(child: ExplorerItem): void { - if (!this.children) { - this.isDirectory = true; - } - // Inherit some parent properties to child child.parent = this; child.updateResource(false); - - if (this.children) { - this.children.set(this.getPlatformAwareName(child.name), child); - } + this.children.set(this.getPlatformAwareName(child.name), child); } getChild(name: string): ExplorerItem | undefined { - if (!this.children) { - return undefined; - } - return this.children.get(this.getPlatformAwareName(name)); } @@ -273,21 +251,11 @@ export class ExplorerItem { }); } - getChildrenCount(): number { - if (!this.children) { - return 0; - } - - return this.children.size; - } - /** * Removes a child element from this folder. */ removeChild(child: ExplorerItem): void { - if (this.children) { - this.children.delete(this.getPlatformAwareName(child.name)); - } + this.children.delete(this.getPlatformAwareName(child.name)); } private getPlatformAwareName(name: string): string { @@ -308,7 +276,7 @@ export class ExplorerItem { this.resource = resources.joinPath(this.parent.resource, this.name); if (recursive) { - if (this.isDirectory && this.children) { + if (this.isDirectory) { this.children.forEach(child => { child.updateResource(true); }); @@ -350,7 +318,7 @@ export class ExplorerItem { return this; } - if (this.children) { + if (this.isDirectory) { // Ignore separtor to more easily deduct the next name to search while (index < path.length && path[index] === paths.sep) { index++; @@ -397,7 +365,6 @@ export class NewStatPlaceholder extends ExplorerItem { this.parent.removeChild(this); this.isDirectoryResolved = false; - this.isDirectory = false; } getId(): string { diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index b4133dfedd2..5e06f2fbe26 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isUndefinedOrNull } from 'vs/base/common/types'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; @@ -35,10 +34,8 @@ suite('Files - View Model', () => { assert.strictEqual(s.name, 'sName'); assert.strictEqual(s.isDirectory, true); assert.strictEqual(s.mtime, new Date(d).getTime()); - assert.strictEqual(s.getChildrenArray().length, 0); s = createStat('/path/to/stat', 'sName', false, false, 8096, d); - assert(isUndefinedOrNull(s.getChildrenArray())); }); test('Add and Remove Child, check for hasChild', function () { @@ -50,14 +47,14 @@ suite('Files - View Model', () => { s.addChild(child1); - assert(s.getChildrenArray().length === 1); + assert(!!s.getChild(child1.name)); s.removeChild(child1); s.addChild(child1); - assert(s.getChildrenArray().length === 1); + assert(!!s.getChild(child1.name)); s.removeChild(child1); - assert(s.getChildrenArray().length === 0); + assert(!s.getChild(child1.name)); // Assert that adding a child updates its path properly s.addChild(child4); @@ -78,10 +75,6 @@ suite('Files - View Model', () => { s4.move(s1); - assert.strictEqual(s3.getChildrenArray().length, 0); - - assert.strictEqual(s1.getChildrenArray().length, 2); - // Assert the new path of the moved element assert.strictEqual(s4.resource.fsPath, toResource('/' + s4.name).fsPath); @@ -159,7 +152,7 @@ suite('Files - View Model', () => { assert.strictEqual(s1.find(s4Upper.resource), s4); } - assert.strictEqual(s1.find(toResource('foobar')), null); + assert.strictEqual(s1.find(toResource('foobar')), undefined); assert.strictEqual(s1.find(toResource('/')), s1); assert.strictEqual(s1.find(toResource('')), s1); @@ -277,7 +270,6 @@ suite('Files - View Model', () => { // Merge Child when isDirectoryResolved=false is a no-op merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d)); ExplorerItem.mergeLocalWithDisk(merge2, merge1); - assert.strictEqual(merge1.getChildrenArray().length, 0); // Merge Child with isDirectoryResolved=true const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d); @@ -285,7 +277,6 @@ suite('Files - View Model', () => { merge2.addChild(child); merge2.isDirectoryResolved = true; ExplorerItem.mergeLocalWithDisk(merge2, merge1); - assert.strictEqual(merge1.getChildrenArray().length, 1); assert.strictEqual(merge1.getChild('foo.html').name, 'foo.html'); assert.deepEqual(merge1.getChild('foo.html').parent, merge1, 'Check parent'); From 5915e1cb49fc054ad6ca5c0f845f56cbfc191769 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 13:57:02 +0100 Subject: [PATCH 24/65] explorer service: reorder methods --- .../files/electron-browser/explorerService.ts | 18 +++++---- .../electron-browser/views/explorerView.ts | 38 +++++++++---------- .../electron-browser/explorerModel.test.ts | 1 - 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 69fa9cf4aee..010305f551f 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -84,6 +84,8 @@ export class ExplorerService implements IExplorerService { return model; } + // IExplorerService methods + findClosest(resource: URI): ExplorerItem { return this.model.findClosest(resource); } @@ -121,14 +123,6 @@ export class ExplorerService implements IExplorerService { }, e => { this.notificationService.error(e); }); } - private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { - const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; - if (this.sortOrder !== configSortOrder) { - this.sortOrder = configSortOrder; - this.roots.forEach(r => this._onDidChangeItem.fire(r)); - } - } - // File events private onFileOperation(e: FileOperationEvent): void { @@ -297,6 +291,14 @@ export class ExplorerService implements IExplorerService { })); } + private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { + const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; + if (this.sortOrder !== configSortOrder) { + this.sortOrder = configSortOrder; + this.roots.forEach(r => this._onDidChangeItem.fire(r)); + } + } + dispose(): void { dispose(this.disposables); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 1e29f06b1aa..9fb1f7a9dfa 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -198,18 +198,6 @@ export class ExplorerView extends ViewletPanel { } } - private getActiveFile(): URI { - const input = this.editorService.activeEditor; - - // ignore diff editor inputs (helps to get out of diffing when returning to explorer) - if (input instanceof DiffEditorInput) { - return undefined; - } - - // check for files - return toResource(input, { supportSideBySide: true }); - } - private createTree(container: HTMLElement): void { this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); @@ -276,13 +264,6 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.tree.onContextMenu(e => this.onContextMenu(e))); } - getOptimalWidth(): number { - const parentNode = this.tree.getHTMLElement(); - const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels - - return DOM.getLargestChildWidth(parentNode, childNodes); - } - // React on events private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { @@ -357,6 +338,13 @@ export class ExplorerView extends ViewletPanel { return this.tree.refresh(toRefresh, true); } + getOptimalWidth(): number { + const parentNode = this.tree.getHTMLElement(); + const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels + + return DOM.getLargestChildWidth(parentNode, childNodes); + } + private setTreeInput(newRoots?: IWorkspaceFolder[]): Promise { if (!this.isVisible()) { this.shouldRefresh = true; @@ -392,6 +380,18 @@ export class ExplorerView extends ViewletPanel { return promise; } + private getActiveFile(): URI { + const input = this.editorService.activeEditor; + + // ignore diff editor inputs (helps to get out of diffing when returning to explorer) + if (input instanceof DiffEditorInput) { + return undefined; + } + + // check for files + return toResource(input, { supportSideBySide: true }); + } + private onSelectItem(fileStat: ExplorerItem, reveal = this.autoReveal): Promise { if (!fileStat || !this.isVisible()) { return Promise.resolve(void 0); diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index 5e06f2fbe26..a6dd98de1bf 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -20,7 +20,6 @@ function toResource(path) { } else { return URI.file(join('/home/john', path)); } - } suite('Files - View Model', () => { From 676faed9e744726158296aaba6eafb2758941ad5 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 14:47:32 +0100 Subject: [PATCH 25/65] explorer: return null sometimes over undefined --- .../parts/files/common/explorerModel.ts | 24 +++++++++---------- .../files/electron-browser/explorerService.ts | 6 ++--- .../electron-browser/explorerModel.test.ts | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 83ce59db95d..a27e7df43d7 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -45,7 +45,7 @@ export class ExplorerModel implements IDisposable { * In case multiple FileStat are matching the resource (same folder opened multiple times) returns the FileStat that has the closest root. * Will return undefined in case the FileStat does not exist. */ - findClosest(resource: URI): ExplorerItem | undefined { + findClosest(resource: URI): ExplorerItem | null { const folder = this.contextService.getWorkspaceFolder(resource); if (folder) { const root = this.roots.filter(r => r.resource.toString() === folder.uri.toString()).pop(); @@ -54,7 +54,7 @@ export class ExplorerModel implements IDisposable { } } - return undefined; + return null; } dispose(): void { @@ -68,7 +68,7 @@ export class ExplorerItem { constructor( public resource: URI, - public root: ExplorerItem | undefined, + public root?: ExplorerItem, private _isSymbolicLink?: boolean, private _isReadonly?: boolean, private _isDirectory?: boolean, @@ -232,7 +232,7 @@ export class ExplorerItem { } fetchChildren(fileService: IFileService): Promise { - let promise = Promise.resolve(undefined); + let promise = Promise.resolve(null); if (!this.isDirectoryResolved) { promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { const resolved = ExplorerItem.create(stat, this.root); @@ -300,9 +300,9 @@ export class ExplorerItem { /** * Returns a child stat from this stat that matches with the provided path. - * Will return "undefined" in case the child does not exist. + * Will return "null" in case the child does not exist. */ - find(resource: URI): ExplorerItem | undefined { + find(resource: URI): ExplorerItem | null { // Return if path found // For performance reasons try to do the comparison as fast as possible if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && @@ -310,10 +310,10 @@ export class ExplorerItem { return this.findByPath(rtrim(resource.path, paths.sep), this.resource.path.length); } - return undefined; //Unable to find + return null; //Unable to find } - private findByPath(path: string, index: number): ExplorerItem | undefined { + private findByPath(path: string, index: number): ExplorerItem | null { if (paths.isEqual(rtrim(this.resource.path, paths.sep), path, !isLinux)) { return this; } @@ -340,7 +340,7 @@ export class ExplorerItem { } } - return undefined; + return null; } } @@ -353,7 +353,7 @@ export class NewStatPlaceholder extends ExplorerItem { private id: number; private directoryPlaceholder: boolean; - constructor(isDirectory: boolean, root: ExplorerItem | undefined) { + constructor(isDirectory: boolean, root: ExplorerItem) { super(URI.file(''), root, false, false, false, NewStatPlaceholder.NAME); this.id = NewStatPlaceholder.ID++; @@ -391,8 +391,8 @@ export class NewStatPlaceholder extends ExplorerItem { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } - find(resource: URI): ExplorerItem | undefined { - return undefined; + find(resource: URI): ExplorerItem | null { + return null; } static addNewStatPlaceholder(parent: ExplorerItem, isDirectory: boolean): NewStatPlaceholder { diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 010305f551f..dfe61969f5f 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -30,7 +30,7 @@ export class ExplorerService implements IExplorerService { private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first - private _onDidChangeItem = new Emitter(); + private _onDidChangeItem = new Emitter(); private _onDidChangeEditable = new Emitter(); private _onDidSelectItem = new Emitter<{ item: ExplorerItem, reveal: boolean }>(); private disposables: IDisposable[] = []; @@ -49,7 +49,7 @@ export class ExplorerService implements IExplorerService { return this.model.roots; } - get onDidChangeItem(): Event { + get onDidChangeItem(): Event { return this._onDidChangeItem.event; } @@ -79,7 +79,7 @@ export class ExplorerService implements IExplorerService { this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); - this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this._onDidChangeItem.fire())); + this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this._onDidChangeItem.fire(null))); return model; } diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index a6dd98de1bf..b960ab81444 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -11,7 +11,7 @@ import { validateFileName } from 'vs/workbench/parts/files/electron-browser/file import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource(path), undefined, false, false, isFolder, name, mtime); + return new ExplorerItem(toResource(path), null, false, false, isFolder, name, mtime); } function toResource(path) { @@ -151,7 +151,7 @@ suite('Files - View Model', () => { assert.strictEqual(s1.find(s4Upper.resource), s4); } - assert.strictEqual(s1.find(toResource('foobar')), undefined); + assert.strictEqual(s1.find(toResource('foobar')), null); assert.strictEqual(s1.find(toResource('/')), s1); assert.strictEqual(s1.find(toResource('')), s1); From ced64d2245d195cfd2b375753c481c1b5c372c2d Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 15:17:38 +0100 Subject: [PATCH 26/65] explorer: fix focus issues --- .../files/electron-browser/explorerService.ts | 2 +- .../files/electron-browser/fileActions.ts | 8 +------ .../electron-browser/views/explorerView.ts | 21 +++++++++++++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index dfe61969f5f..032afd931e9 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -103,7 +103,7 @@ export class ExplorerService implements IExplorerService { const fileStat = this.findClosest(resource); if (fileStat) { this._onDidSelectItem.fire({ item: fileStat, reveal }); - return Promise.resolve(void 0); + return Promise.resolve(null); } // Stat needs to be resolved first and then revealed diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 7764b2cbbf2..fb9cee81b7e 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -1136,13 +1136,7 @@ export class FocusFilesExplorer extends Action { } public run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true).then((viewlet: ExplorerViewlet) => { - const view = viewlet.getExplorerView(); - if (view) { - view.setExpanded(true); - view.focus(); - } - }); + return this.viewletService.openViewlet(VIEWLET_ID, true); } } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 9fb1f7a9dfa..44a2427898d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -170,7 +170,16 @@ export class ExplorerView extends ViewletPanel { this.onConfigurationUpdated(configuration); // When the explorer viewer is loaded, listen to changes to the editor input - this.disposables.push(this.editorService.onDidActiveEditorChange(() => this.explorerService.select(this.getActiveFile()))); + this.disposables.push(this.editorService.onDidActiveEditorChange(() => { + if (this.autoReveal) { + const activeFile = this.getActiveFile(); + if (activeFile) { + this.explorerService.select(this.getActiveFile()); + } else { + this.tree.setSelection([]); + } + } + })); // Also handle configuration updates this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); @@ -187,6 +196,10 @@ export class ExplorerView extends ViewletPanel { return actions; } + focus(): void { + this.tree.domFocus(); + } + setVisible(visible: boolean): void { super.setVisible(visible); if (visible) { @@ -235,6 +248,10 @@ export class ExplorerView extends ViewletPanel { // Open when selecting via keyboard this.disposables.push(this.tree.onDidChangeSelection(e => { + if (!e.browserEvent) { + // Only react on selection change events caused by user interaction (ignore those which are caused by us doing tree.setSelection). + return; + } const selection = e.elements; // Do not react if the user is expanding selection if (selection && selection.length === 1) { @@ -256,7 +273,7 @@ export class ExplorerView extends ViewletPanel { "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } }*/ this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: (e.browserEvent instanceof MouseEvent) && !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } } })); From d9726e9e2adfef8fd3a42bd739c50a67daa082c7 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 15:38:09 +0100 Subject: [PATCH 27/65] explorer: simplify refresh --- .../electron-browser/views/explorerView.ts | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 44a2427898d..0b28669c009 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; -import { ThrottledDelayer, sequence, ignoreErrors } from 'vs/base/common/async'; +import { sequence, ignoreErrors } from 'vs/base/common/async'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; @@ -44,13 +44,10 @@ import { onUnexpectedError } from 'vs/base/common/errors'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; - private static readonly EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes private tree: WorkbenchAsyncDataTree; private filter: FilesFilter; - private explorerRefreshDelayer: ThrottledDelayer; - private resourceContext: ResourceContextKey; private folderContext: IContextKey; private readonlyContext: IContextKey; @@ -84,8 +81,6 @@ export class ExplorerView extends ViewletPanel { ) { super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); - this.explorerRefreshDelayer = new ThrottledDelayer(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY); - this.resourceContext = instantiationService.createInstance(ResourceContextKey); this.disposables.push(this.resourceContext); this.folderContext = ExplorerFolderContext.bindTo(contextKeyService); @@ -158,10 +153,10 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.setTreeInput())); this.disposables.push(this.labelService.onDidRegisterFormatter(() => { this._onDidChangeTitleArea.fire(); - this.refreshFromEvent(); + this.refresh(); })); - this.disposables.push(this.explorerService.onDidChangeItem(e => this.refreshFromEvent(e))); + this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e))); this.disposables.push(this.explorerService.onDidChangeEditable(e => this.refresh(e.parent))); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); @@ -303,16 +298,6 @@ export class ExplorerView extends ViewletPanel { } } - private refreshFromEvent(explorerItem?: ExplorerItem): void { - if (this.isVisible()) { - this.explorerRefreshDelayer.trigger(() => { - return this.refresh(explorerItem); - }); - } else { - this.shouldRefresh = true; - } - } - private onContextMenu(e: ITreeContextMenuEvent): void { const stat = e.element; if (stat instanceof NewStatPlaceholder) { @@ -347,7 +332,8 @@ export class ExplorerView extends ViewletPanel { * Refresh the contents of the explorer to get up to date data from the disk about the file structure. */ refresh(item?: ExplorerItem): Promise { - if (!this.tree) { + if (!this.tree || !this.isVisible()) { + this.shouldRefresh = true; return Promise.resolve(void 0); } const toRefresh = item || this.tree.getInput(); From a8b16bd91bef8b104dabda9adfe1755345c577da Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 16:11:32 +0100 Subject: [PATCH 28/65] explorer: sorter --- src/vs/workbench/parts/files/common/files.ts | 1 + .../files/electron-browser/explorerService.ts | 14 +- .../files/electron-browser/fileActions.ts | 4 +- .../electron-browser/views/explorerView.ts | 5 +- .../electron-browser/views/explorerViewer.ts | 162 ++++++++---------- 5 files changed, 87 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 45787cca779..fa8e44ea004 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -41,6 +41,7 @@ export interface IEditableData { export interface IExplorerService { _serviceBrand: any; readonly roots: ExplorerItem[]; + readonly sortOrder: SortOrder; readonly onDidChangeItem: Event; readonly onDidChangeEditable: Event; readonly onDidSelectItem: Event<{ item: ExplorerItem, reveal: boolean }>; diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 032afd931e9..6e2b3582f97 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -35,7 +35,7 @@ export class ExplorerService implements IExplorerService { private _onDidSelectItem = new Emitter<{ item: ExplorerItem, reveal: boolean }>(); private disposables: IDisposable[] = []; private editableStats = new Map(); - private sortOrder: SortOrder; + private _sortOrder: SortOrder; constructor( @IFileService private fileService: IFileService, @@ -61,6 +61,10 @@ export class ExplorerService implements IExplorerService { return this._onDidSelectItem.event; } + get sortOrder(): SortOrder { + return this._sortOrder; + } + // Memoized locals @memoize private get fileEventsFilter(): ResourceGlobMatcher { const fileEventsFilter = this.instantiationService.createInstance( @@ -256,7 +260,7 @@ export class ExplorerService implements IExplorerService { } // Handle updated files/folders if we sort by modified - if (this.sortOrder === SortOrderConfiguration.MODIFIED) { + if (this._sortOrder === SortOrderConfiguration.MODIFIED) { const updated = e.getUpdated(); // Check updated: Refresh if updated file/folder part of resolved root @@ -275,7 +279,7 @@ export class ExplorerService implements IExplorerService { private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { return new FileChangesEvent(e.changes.filter(change => { - if (change.type === FileChangeType.UPDATED && this.sortOrder !== SortOrderConfiguration.MODIFIED) { + if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrderConfiguration.MODIFIED) { return false; // we only are about updated if we sort by modified time } @@ -293,8 +297,8 @@ export class ExplorerService implements IExplorerService { private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; - if (this.sortOrder !== configSortOrder) { - this.sortOrder = configSortOrder; + if (this._sortOrder !== configSortOrder) { + this._sortOrder = configSortOrder; this.roots.forEach(r => this._onDidChangeItem.fire(r)); } } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index fb9cee81b7e..5bc9f2a8206 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -804,7 +804,7 @@ class PasteFileAction extends BaseFileAction { // Copy File return this.fileService.copyFile(fileToPaste, targetFile).then(stat => { if (!stat.isDirectory) { - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); + return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }); } return void 0; @@ -838,7 +838,7 @@ export class DuplicateFileAction extends BaseFileAction { // Copy File const result = this.fileService.copyFile(this.element.resource, findValidPasteFileTarget(this.target, { resource: this.element.resource, isDirectory: this.element.isDirectory })).then(stat => { if (!stat.isDirectory) { - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); + return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }); } return void 0; diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 0b28669c009..0a7937330e8 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -31,7 +31,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -222,7 +222,8 @@ export class ExplorerView extends ViewletPanel { keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: stat => stat.name }, - filter: this.filter + filter: this.filter, + sorter: this.instantiationService.createInstance(FileSorter) }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.disposables.push(this.tree); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 4ee028de2e6..5e99764c13c 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -15,7 +15,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,6 +33,7 @@ import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; import { IAction } from 'vs/base/common/actions'; import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -352,117 +353,98 @@ export class FilesFilter implements ITreeFilter { } // // Explorer Sorter -// export class FileSorter implements ISorter { -// private toDispose: IDisposable[]; -// private sortOrder: SortOrder; +export class FileSorter implements ITreeSorter { -// constructor( -// @IConfigurationService private configurationService: IConfigurationService, -// @IWorkspaceContextService private contextService: IWorkspaceContextService -// ) { -// this.toDispose = []; + constructor( + @IExplorerService private explorerService: IExplorerService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { } -// this.updateSortOrder(); + public compare(statA: ExplorerItem, statB: ExplorerItem): number { + // Do not sort roots + if (statA.isRoot) { + if (statB.isRoot) { + return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index; + } -// this.registerListeners(); -// } + return -1; + } -// private registerListeners(): void { -// this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateSortOrder())); -// } + if (statB.isRoot) { + return 1; + } -// private updateSortOrder(): void { -// this.sortOrder = this.configurationService.getValue('explorer.sortOrder') || 'default'; -// } + const sortOrder = this.explorerService.sortOrder; -// public compare(tree: ITree, statA: ExplorerItem, statB: ExplorerItem): number { + // Sort Directories + switch (sortOrder) { + case 'type': + if (statA.isDirectory && !statB.isDirectory) { + return -1; + } -// // Do not sort roots -// if (statA.isRoot) { -// if (statB.isRoot) { -// return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index; -// } + if (statB.isDirectory && !statA.isDirectory) { + return 1; + } -// return -1; -// } + if (statA.isDirectory && statB.isDirectory) { + return compareFileNames(statA.name, statB.name); + } -// if (statB.isRoot) { -// return 1; -// } + break; -// // Sort Directories -// switch (this.sortOrder) { -// case 'type': -// if (statA.isDirectory && !statB.isDirectory) { -// return -1; -// } + case 'filesFirst': + if (statA.isDirectory && !statB.isDirectory) { + return 1; + } -// if (statB.isDirectory && !statA.isDirectory) { -// return 1; -// } + if (statB.isDirectory && !statA.isDirectory) { + return -1; + } -// if (statA.isDirectory && statB.isDirectory) { -// return comparers.compareFileNames(statA.name, statB.name); -// } + break; -// break; + case 'mixed': + break; // not sorting when "mixed" is on -// case 'filesFirst': -// if (statA.isDirectory && !statB.isDirectory) { -// return 1; -// } + default: /* 'default', 'modified' */ + if (statA.isDirectory && !statB.isDirectory) { + return -1; + } -// if (statB.isDirectory && !statA.isDirectory) { -// return -1; -// } + if (statB.isDirectory && !statA.isDirectory) { + return 1; + } -// break; + break; + } -// case 'mixed': -// break; // not sorting when "mixed" is on + // Sort "New File/Folder" placeholders + if (statA instanceof NewStatPlaceholder) { + return -1; + } -// default: /* 'default', 'modified' */ -// if (statA.isDirectory && !statB.isDirectory) { -// return -1; -// } + if (statB instanceof NewStatPlaceholder) { + return 1; + } -// if (statB.isDirectory && !statA.isDirectory) { -// return 1; -// } + // Sort Files + switch (sortOrder) { + case 'type': + return compareFileExtensions(statA.name, statB.name); -// break; -// } + case 'modified': + if (statA.mtime !== statB.mtime) { + return statA.mtime < statB.mtime ? 1 : -1; + } -// // Sort "New File/Folder" placeholders -// if (statA instanceof NewStatPlaceholder) { -// return -1; -// } + return compareFileNames(statA.name, statB.name); -// if (statB instanceof NewStatPlaceholder) { -// return 1; -// } - -// // Sort Files -// switch (this.sortOrder) { -// case 'type': -// return comparers.compareFileExtensions(statA.name, statB.name); - -// case 'modified': -// if (statA.mtime !== statB.mtime) { -// return statA.mtime < statB.mtime ? 1 : -1; -// } - -// return comparers.compareFileNames(statA.name, statB.name); - -// default: /* 'default', 'mixed', 'filesFirst' */ -// return comparers.compareFileNames(statA.name, statB.name); -// } -// } - -// public dispose(): void { -// this.toDispose = dispose(this.toDispose); -// } -// } + default: /* 'default', 'mixed', 'filesFirst' */ + return compareFileNames(statA.name, statB.name); + } + } +} // export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { From c075795f7e2a1d6f60891299859507bcd1204937 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 17:01:53 +0100 Subject: [PATCH 29/65] file actions: some cleanup --- .../files/electron-browser/fileActions.ts | 172 ++++-------------- 1 file changed, 34 insertions(+), 138 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 5bc9f2a8206..cd753f21ff5 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -35,7 +35,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IListService, ListWidget } from 'vs/platform/list/browser/listService'; -import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Schemas } from 'vs/base/common/network'; import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -100,49 +100,22 @@ export class BaseErrorReportingAction extends Action { } } -export class BaseFileAction extends BaseErrorReportingAction { - public element: ExplorerItem; - - constructor( - id: string, - label: string, - @IFileService protected fileService: IFileService, - @INotificationService notificationService: INotificationService, - @ITextFileService protected textFileService: ITextFileService - ) { - super(id, label, notificationService); - - this.enabled = false; - } - - _isEnabled(): boolean { - return true; - } - - _updateEnablement(): void { - this.enabled = !!(this.fileService && this._isEnabled()); - } -} - -class TriggerRenameFileAction extends BaseFileAction { +class TriggerRenameFileAction extends BaseErrorReportingAction { public static readonly ID = 'renameFile'; private renameAction: BaseRenameAction; constructor( - element: ExplorerItem, - @IFileService fileService: IFileService, + private element: ExplorerItem, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, @IInstantiationService instantiationService: IInstantiationService, @IExplorerService private explorerService: IExplorerService ) { - super(TriggerRenameFileAction.ID, TRIGGER_RENAME_LABEL, fileService, notificationService, textFileService); + super(TriggerRenameFileAction.ID, TRIGGER_RENAME_LABEL, notificationService); this.element = element; this.renameAction = instantiationService.createInstance(RenameFileAction, element); - this._updateEnablement(); } public validateFileName(name: string): string { @@ -163,23 +136,16 @@ class TriggerRenameFileAction extends BaseFileAction { } } -export abstract class BaseRenameAction extends BaseFileAction { +export abstract class BaseRenameAction extends BaseErrorReportingAction { constructor( id: string, label: string, - element: ExplorerItem, - @IFileService fileService: IFileService, + public element: ExplorerItem, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService ) { - super(id, label, fileService, notificationService, textFileService); - - this.element = element; - } - - _isEnabled(): boolean { - return super._isEnabled() && this.element && !this.element.isReadonly; + super(id, label, notificationService); + this.enabled = this.element && !this.element.isReadonly; } public run(context?: any): Promise { @@ -234,13 +200,10 @@ class RenameFileAction extends BaseRenameAction { constructor( element: ExplorerItem, - @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService + @ITextFileService private textFileService: ITextFileService ) { - super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, fileService, notificationService, textFileService); - - this._updateEnablement(); + super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, notificationService); } public runAction(newName: string): Promise { @@ -252,7 +215,7 @@ class RenameFileAction extends BaseRenameAction { } /* Base New File/Folder Action */ -export class BaseNewAction extends BaseFileAction { +export class BaseNewAction extends BaseErrorReportingAction { private presetFolder: ExplorerItem; constructor( @@ -261,12 +224,10 @@ export class BaseNewAction extends BaseFileAction { private isFile: boolean, private renameAction: BaseRenameAction, element: ExplorerItem, - @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, @IExplorerService private explorerService: IExplorerService ) { - super(id, label, fileService, notificationService, textFileService); + super(id, label, notificationService); if (element) { this.presetFolder = element.isDirectory ? element : element.parent; @@ -299,19 +260,14 @@ export class BaseNewAction extends BaseFileAction { /* New File */ export class NewFileAction extends BaseNewAction { - constructor( element: ExplorerItem, - @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, @IInstantiationService instantiationService: IInstantiationService, @IExplorerService explorerService: IExplorerService ) { - super('explorer.newFile', NEW_FILE_LABEL, true, instantiationService.createInstance(CreateFileAction, element), null, fileService, notificationService, textFileService, explorerService); - + super('explorer.newFile', NEW_FILE_LABEL, true, instantiationService.createInstance(CreateFileAction, element), null, notificationService, explorerService); this.class = 'explorer-action new-file'; - this._updateEnablement(); } } @@ -320,16 +276,12 @@ export class NewFolderAction extends BaseNewAction { constructor( element: ExplorerItem, - @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, @IInstantiationService instantiationService: IInstantiationService, @IExplorerService explorerService: IExplorerService ) { - super('explorer.newFolder', NEW_FOLDER_LABEL, false, instantiationService.createInstance(CreateFolderAction, element), null, fileService, notificationService, textFileService, explorerService); - + super('explorer.newFolder', NEW_FOLDER_LABEL, false, instantiationService.createInstance(CreateFolderAction, element), null, notificationService, explorerService); this.class = 'explorer-action new-folder'; - this._updateEnablement(); } } @@ -371,14 +323,11 @@ class CreateFileAction extends BaseCreateAction { constructor( element: ExplorerItem, - @IFileService fileService: IFileService, @IEditorService private editorService: IEditorService, + @IFileService private fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService ) { - super(CreateFileAction.ID, CreateFileAction.LABEL, element, fileService, notificationService, textFileService); - - this._updateEnablement(); + super(CreateFileAction.ID, CreateFileAction.LABEL, element, notificationService); } public runAction(fileName: string): Promise { @@ -399,13 +348,10 @@ class CreateFolderAction extends BaseCreateAction { constructor( element: ExplorerItem, - @IFileService fileService: IFileService, + @IFileService private fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService ) { - super(CreateFolderAction.ID, CreateFolderAction.LABEL, null, fileService, notificationService, textFileService); - - this._updateEnablement(); + super(CreateFolderAction.ID, CreateFolderAction.LABEL, element, notificationService); } public runAction(fileName: string): Promise { @@ -416,7 +362,7 @@ class CreateFolderAction extends BaseCreateAction { } } -class BaseDeleteFileAction extends BaseFileAction { +class BaseDeleteFileAction extends BaseErrorReportingAction { private static readonly CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; @@ -425,21 +371,16 @@ class BaseDeleteFileAction extends BaseFileAction { constructor( private elements: ExplorerItem[], private useTrash: boolean, - @IFileService fileService: IFileService, + @IFileService private fileService: IFileService, @INotificationService notificationService: INotificationService, @IDialogService private dialogService: IDialogService, - @ITextFileService textFileService: ITextFileService, + @ITextFileService private textFileService: ITextFileService, @IConfigurationService private configurationService: IConfigurationService ) { - super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, fileService, notificationService, textFileService); + super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, notificationService); this.useTrash = useTrash && elements.every(e => !paths.isUNC(e.resource.fsPath)); // on UNC shares there is no trash - - this._updateEnablement(); - } - - _isEnabled(): boolean { - return super._isEnabled() && this.elements && this.elements.every(e => !e.isReadonly); + this.enabled = this.elements && this.elements.every(e => !e.isReadonly); } public run(): Promise { @@ -631,28 +572,24 @@ class BaseDeleteFileAction extends BaseFileAction { } /* Add File */ -export class AddFilesAction extends BaseFileAction { +export class AddFilesAction extends BaseErrorReportingAction { constructor( - element: ExplorerItem, + private element: ExplorerItem, clazz: string, - @IFileService fileService: IFileService, + @IFileService private fileService: IFileService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, + @ITextFileService private textFileService: ITextFileService, @IExplorerService private explorerService: IExplorerService ) { - super('workbench.files.action.addFile', nls.localize('addFiles', "Add Files"), fileService, notificationService, textFileService); - - this.element = element; + super('workbench.files.action.addFile', nls.localize('addFiles', "Add Files"), notificationService); if (clazz) { this.class = clazz; } - - this._updateEnablement(); } public run(resourcesToAdd: URI[]): Promise { @@ -736,19 +673,14 @@ export class AddFilesAction extends BaseFileAction { } // Copy File/Folder -class CopyFileAction extends BaseFileAction { +class CopyFileAction extends BaseErrorReportingAction { constructor( private elements: ExplorerItem[], - @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, - @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService private clipboardService: IClipboardService ) { - super('filesExplorer.copy', COPY_FILE_LABEL, fileService, notificationService, textFileService); - - this._updateEnablement(); + super('filesExplorer.copy', COPY_FILE_LABEL, notificationService); } public run(): Promise { @@ -761,25 +693,22 @@ class CopyFileAction extends BaseFileAction { } // Paste File/Folder -class PasteFileAction extends BaseFileAction { +class PasteFileAction extends BaseErrorReportingAction { public static readonly ID = 'filesExplorer.paste'; constructor( - element: ExplorerItem, - @IFileService fileService: IFileService, + private element: ExplorerItem, + @IFileService private fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, @IEditorService private editorService: IEditorService, @IExplorerService private explorerService: IExplorerService ) { - super(PasteFileAction.ID, PASTE_FILE_LABEL, fileService, notificationService, textFileService); + super(PasteFileAction.ID, PASTE_FILE_LABEL, notificationService); - this.element = element; if (!this.element) { this.element = this.explorerService.roots[0]; } - this._updateEnablement(); } public run(fileToPaste: URI): Promise { @@ -815,39 +744,6 @@ class PasteFileAction extends BaseFileAction { } } -// Duplicate File/Folder -export class DuplicateFileAction extends BaseFileAction { - private target: ExplorerItem; - - constructor( - fileToDuplicate: ExplorerItem, - target: ExplorerItem, - @IFileService fileService: IFileService, - @IEditorService private editorService: IEditorService, - @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService - ) { - super('workbench.files.action.duplicateFile', nls.localize('duplicateFile', "Duplicate"), fileService, notificationService, textFileService); - - this.element = fileToDuplicate; - this.target = (target && target.isDirectory) ? target : fileToDuplicate.parent; - this._updateEnablement(); - } - - public run(): Promise { - // Copy File - const result = this.fileService.copyFile(this.element.resource, findValidPasteFileTarget(this.target, { resource: this.element.resource, isDirectory: this.element.isDirectory })).then(stat => { - if (!stat.isDirectory) { - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }); - } - - return void 0; - }, error => this.onError(error)); - - return result; - } -} - function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean }): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); From fac0722dc8f931d2680ad3bcb80521a075a3210c Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 Dec 2018 18:00:45 +0100 Subject: [PATCH 30/65] explorer: input box shananigens --- .../parts/files/common/explorerModel.ts | 97 ++++--------------- .../files/electron-browser/explorerService.ts | 6 +- .../files/electron-browser/fileActions.ts | 29 ++---- .../electron-browser/views/explorerView.ts | 5 +- .../electron-browser/views/explorerViewer.ts | 21 ++-- .../electron-browser/explorerModel.test.ts | 10 +- 6 files changed, 48 insertions(+), 120 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index a27e7df43d7..5a351c78900 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -22,7 +22,7 @@ export class ExplorerModel implements IDisposable { constructor(private contextService: IWorkspaceContextService) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, undefined, false, false, true, folder.name)); + .map(folder => new ExplorerItem(folder.uri, undefined, true, false, false, folder.name)); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => setRoots()); setRoots(); } @@ -63,23 +63,19 @@ export class ExplorerModel implements IDisposable { } export class ExplorerItem { - public parent: ExplorerItem; public isDirectoryResolved: boolean; constructor( public resource: URI, - public root?: ExplorerItem, + private _parent: ExplorerItem, + private _isDirectory?: boolean, private _isSymbolicLink?: boolean, private _isReadonly?: boolean, - private _isDirectory?: boolean, private _name: string = resources.basenameOrAuthority(resource), private _mtime?: number, private _etag?: string, private _isError?: boolean ) { - if (!this.root) { - this.root = this; - } this.isDirectoryResolved = false; } @@ -111,6 +107,18 @@ export class ExplorerItem { return this._name; } + get parent(): ExplorerItem { + return this._parent; + } + + get root(): ExplorerItem { + if (!this._parent) { + return this; + } + + return this.parent.root; + } + @memoize get children(): Map { return new Map(); } @@ -134,8 +142,8 @@ export class ExplorerItem { return this === this.root; } - static create(raw: IFileStat, root: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { - const stat = new ExplorerItem(raw.resource, root, raw.isSymbolicLink, raw.isReadonly, raw.isDirectory, raw.name, raw.mtime, raw.etag, isError); + static create(raw: IFileStat, parent: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { + const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, raw.isReadonly, raw.name, raw.mtime, raw.etag, isError); // Recursively add children if present if (stat.isDirectory) { @@ -150,8 +158,7 @@ export class ExplorerItem { // Recurse into children if (raw.children) { for (let i = 0, len = raw.children.length; i < len; i++) { - const child = ExplorerItem.create(raw.children[i], root, resolveTo); - child.parent = stat; + const child = ExplorerItem.create(raw.children[i], stat, resolveTo); stat.addChild(child); } } @@ -204,13 +211,11 @@ export class ExplorerItem { // Existing child: merge if (formerLocalChild) { ExplorerItem.mergeLocalWithDisk(diskChild, formerLocalChild); - formerLocalChild.parent = local; local.addChild(formerLocalChild); } // New child: add else { - diskChild.parent = local; local.addChild(diskChild); } }); @@ -222,7 +227,7 @@ export class ExplorerItem { */ addChild(child: ExplorerItem): void { // Inherit some parent properties to child - child.parent = this; + child._parent = this; child.updateResource(false); this.children.set(this.getPlatformAwareName(child.name), child); } @@ -235,7 +240,7 @@ export class ExplorerItem { let promise = Promise.resolve(null); if (!this.isDirectoryResolved) { promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { - const resolved = ExplorerItem.create(stat, this.root); + const resolved = ExplorerItem.create(stat, this); ExplorerItem.mergeLocalWithDisk(resolved, this); this.isDirectoryResolved = true; }); @@ -343,65 +348,3 @@ export class ExplorerItem { return null; } } - -/* A helper that can be used to show a placeholder when creating a new stat */ -export class NewStatPlaceholder extends ExplorerItem { - - static readonly NAME = ''; - private static ID = 0; - - private id: number; - private directoryPlaceholder: boolean; - - constructor(isDirectory: boolean, root: ExplorerItem) { - super(URI.file(''), root, false, false, false, NewStatPlaceholder.NAME); - - this.id = NewStatPlaceholder.ID++; - this.isDirectoryResolved = isDirectory; - this.directoryPlaceholder = isDirectory; - } - - destroy(): void { - this.parent.removeChild(this); - - this.isDirectoryResolved = false; - } - - getId(): string { - return `new-stat-placeholder:${this.id}:${this.parent.resource.toString()}`; - } - - isDirectoryPlaceholder(): boolean { - return this.directoryPlaceholder; - } - - addChild() { - throw new Error('Can\'t perform operations in NewStatPlaceholder.'); - } - - removeChild() { - throw new Error('Can\'t perform operations in NewStatPlaceholder.'); - } - - move() { - throw new Error('Can\'t perform operations in NewStatPlaceholder.'); - } - - rename() { - throw new Error('Can\'t perform operations in NewStatPlaceholder.'); - } - - find(resource: URI): ExplorerItem | null { - return null; - } - - static addNewStatPlaceholder(parent: ExplorerItem, isDirectory: boolean): NewStatPlaceholder { - const child = new NewStatPlaceholder(isDirectory, parent.root); - - // Inherit some parent properties to child - child.parent = parent; - parent.addChild(child); - - return child; - } -} diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 6e2b3582f97..3df4b26d8d1 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -118,7 +118,7 @@ export class ExplorerService implements IExplorerService { // Convert to model const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); - const modelStat = ExplorerItem.create(stat, root, options.resolveTo); + const modelStat = ExplorerItem.create(stat, null, options.resolveTo); // Update Input with disk Stat ExplorerItem.mergeLocalWithDisk(modelStat, root); @@ -144,11 +144,11 @@ export class ExplorerService implements IExplorerService { const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(undefined) : this.fileService.resolveFile(p.resource); thenable.then(stat => { if (stat) { - const modelStat = ExplorerItem.create(stat, p.root); + const modelStat = ExplorerItem.create(stat, p.parent); ExplorerItem.mergeLocalWithDisk(modelStat, p); } - const childElement = ExplorerItem.create(addedElement, p.root); + const childElement = ExplorerItem.create(addedElement, p.parent); // Make sure to remove any previous version of the file if any p.removeChild(childElement); p.addChild(childElement); diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index cd753f21ff5..aa5e42545b2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -45,7 +45,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; export interface IEditableData { action: IAction; @@ -217,6 +217,7 @@ class RenameFileAction extends BaseRenameAction { /* Base New File/Folder Action */ export class BaseNewAction extends BaseErrorReportingAction { private presetFolder: ExplorerItem; + static readonly PLACEHOLDER_URI = URI.file(''); constructor( id: string, @@ -244,13 +245,9 @@ export class BaseNewAction extends BaseErrorReportingAction { if (folder.isReadonly) { return Promise.reject(new Error('Parent folder is readonly.')); } - if (!!folder.getChild(NewStatPlaceholder.NAME)) { - // Do not allow to creatae a new file/folder while in the process of creating a new file/folder #47606 - return Promise.resolve(new Error('Parent folder is already in the process of creating a file')); - } - - const stat = NewStatPlaceholder.addNewStatPlaceholder(folder, !this.isFile); + const stat = new ExplorerItem(BaseNewAction.PLACEHOLDER_URI, folder, !this.isFile); + folder.addChild(stat); this.renameAction.element = stat; this.explorerService.setEditable(stat, { action: this.renameAction, validationMessage: value => this.renameAction.validateFileName(folder, value) }); @@ -303,20 +300,8 @@ export class GlobalNewUntitledFileAction extends Action { } } -/* Create New File/Folder (only used internally by explorerViewer) */ -export abstract class BaseCreateAction extends BaseRenameAction { - - public validateFileName(parent: ExplorerItem, name: string): string { - if (this.element instanceof NewStatPlaceholder) { - return validateFileName(parent, name); - } - - return super.validateFileName(parent, name); - } -} - /* Create New File (only used internally by explorerViewer) */ -class CreateFileAction extends BaseCreateAction { +class CreateFileAction extends BaseRenameAction { public static readonly ID = 'workbench.files.action.createFileFromExplorer'; public static readonly LABEL = nls.localize('createNewFile', "New File"); @@ -332,6 +317,7 @@ class CreateFileAction extends BaseCreateAction { public runAction(fileName: string): Promise { const resource = this.element.parent.resource; + this.element.parent.removeChild(this.element); return this.fileService.createFile(resources.joinPath(resource, fileName)).then(stat => { return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); }, (error) => { @@ -341,7 +327,7 @@ class CreateFileAction extends BaseCreateAction { } /* Create New Folder (only used internally by explorerViewer) */ -class CreateFolderAction extends BaseCreateAction { +class CreateFolderAction extends BaseRenameAction { public static readonly ID = 'workbench.files.action.createFolderFromExplorer'; public static readonly LABEL = nls.localize('createNewFolder', "New Folder"); @@ -356,6 +342,7 @@ class CreateFolderAction extends BaseCreateAction { public runAction(fileName: string): Promise { const resource = this.element.parent.resource; + this.element.parent.removeChild(this.element); return this.fileService.createFolder(resources.joinPath(resource, fileName)).then(void 0, (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); }); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 0a7937330e8..f2d5cd75b5b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -39,7 +39,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; import { onUnexpectedError } from 'vs/base/common/errors'; export class ExplorerView extends ViewletPanel { @@ -301,9 +301,6 @@ export class ExplorerView extends ViewletPanel { private onContextMenu(e: ITreeContextMenuEvent): void { const stat = e.element; - if (stat instanceof NewStatPlaceholder) { - return; - } // update dynamic contexts this.fileCopiedContextKey.set(this.clipboardService.hasResources()); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 5e99764c13c..781dc787609 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -32,7 +32,7 @@ import { rtrim } from 'vs/base/common/strings'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; import { IAction } from 'vs/base/common/actions'; -import { ExplorerItem, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; +import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -154,7 +154,7 @@ export class FilesRenderer implements ITreeRenderer { label.element.style.display = 'none'; - if (stat instanceof NewStatPlaceholder) { - stat.destroy(); - } + if (!commit) { + stat.parent.removeChild(stat); + } if (commit && inputBox.value) { await editableData.action.run({ value: inputBox.value }); } dispose(toDispose); container.removeChild(label.element); - // todo@isidor need to unset editable data + this.explorerService.setEditable(stat, null); }); const toDispose = [ @@ -305,7 +305,8 @@ export class FilesFilter implements ITreeFilter { constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IExplorerService private explorerService: IExplorerService ) { this.hiddenExpressionPerRoot = new Map(); this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration()); @@ -334,7 +335,7 @@ export class FilesFilter implements ITreeFilter { if (parentVisibility === TreeVisibility.Hidden) { return false; } - if (stat instanceof NewStatPlaceholder || stat.isRoot) { + if (this.explorerService.getEditableData(stat) || stat.isRoot) { return true; // always visible } @@ -420,11 +421,11 @@ export class FileSorter implements ITreeSorter { } // Sort "New File/Folder" placeholders - if (statA instanceof NewStatPlaceholder) { + if (this.explorerService.getEditableData(statA)) { return -1; } - if (statB instanceof NewStatPlaceholder) { + if (this.explorerService.getEditableData(statB)) { return 1; } diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index b960ab81444..6f366a97b49 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -11,7 +11,7 @@ import { validateFileName } from 'vs/workbench/parts/files/electron-browser/file import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource(path), null, false, false, isFolder, name, mtime); + return new ExplorerItem(toResource(path), null, isFolder, false, false, name, mtime); } function toResource(path) { @@ -259,19 +259,19 @@ suite('Files - View Model', () => { test('Merge Local with Disk', function () { const d = new Date().toUTCString(); - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, false, true, 'to', Date.now(), d); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, false, true, 'to', Date.now(), new Date(0).toUTCString()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now(), d); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now(), new Date(0).toUTCString()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d)); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now(), d)); ExplorerItem.mergeLocalWithDisk(merge2, merge1); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now(), d); merge2.removeChild(child); merge2.addChild(child); merge2.isDirectoryResolved = true; From 5ca44a5a1221869c6e1bf93c31d11ee2f09cb4ee Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 16:03:25 +0100 Subject: [PATCH 31/65] explorer service lazy instantitation --- .../parts/files/electron-browser/files.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index 2399ebd3771..b325009c33e 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -81,7 +81,7 @@ Registry.as(ViewletExtensions.Viewlets).registerViewlet(new Vie 0 )); -registerSingleton(IExplorerService, ExplorerService); +registerSingleton(IExplorerService, ExplorerService, true); Registry.as(ViewletExtensions.Viewlets).setDefaultViewletId(VIEWLET_ID); From ca426e1fcec667d27bccc78fc01d3a0db40860b8 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 16:13:18 +0100 Subject: [PATCH 32/65] explorer adopt to onDidChangeBodyVisibility --- .../electron-browser/views/explorerView.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index bc3dde7a3f0..19b0a8e1a2b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -179,6 +179,16 @@ export class ExplorerView extends ViewletPanel { // Also handle configuration updates this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue(), e))); + + this.disposables.push(this.onDidChangeBodyVisibility(visible => { + if (visible) { + // If a refresh was requested and we are now visible, run it + if (this.shouldRefresh) { + this.shouldRefresh = false; + this.setTreeInput().then(undefined, onUnexpectedError); + } + } + })); } getActions(): IAction[] { @@ -196,17 +206,6 @@ export class ExplorerView extends ViewletPanel { this.tree.domFocus(); } - setVisible(visible: boolean): void { - super.setVisible(visible); - if (visible) { - // If a refresh was requested and we are now visible, run it - if (this.shouldRefresh) { - this.shouldRefresh = false; - this.setTreeInput().then(undefined, onUnexpectedError); - } - } - } - private createTree(container: HTMLElement): void { this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); From 137248638623c286b83a4a7f4f05121da246b75a Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 16:30:09 +0100 Subject: [PATCH 33/65] manualy call setInput --- .../parts/files/electron-browser/views/explorerView.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 19b0a8e1a2b..8f2efd87c51 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -55,7 +55,7 @@ export class ExplorerView extends ViewletPanel { private rootContext: IContextKey; // Refresh is needed on the initial explorer open - private shouldRefresh = true; + private shouldRefresh; private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private autoReveal = false; @@ -229,6 +229,7 @@ export class ExplorerView extends ViewletPanel { sorter: this.instantiationService.createInstance(FileSorter) }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); + this.setTreeInput(); this.disposables.push(this.tree); // Bind context keys FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); From 3f9bbaebad6d43b1464f9a94332c70a3b205ddad Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 17:16:58 +0100 Subject: [PATCH 34/65] explorer: fix multi root issues --- .../parts/files/common/explorerModel.ts | 12 +++++++- src/vs/workbench/parts/files/common/files.ts | 1 + .../files/electron-browser/explorerService.ts | 11 +++++++- .../electron-browser/views/explorerView.ts | 28 ++++++++++--------- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 5a351c78900..e5a7487021e 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -14,23 +14,33 @@ import { coalesce } from 'vs/base/common/arrays'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { memoize } from 'vs/base/common/decorators'; +import { Emitter, Event } from 'vs/base/common/event'; export class ExplorerModel implements IDisposable { private _roots: ExplorerItem[]; private _listener: IDisposable; + private _onDidChangeRoots = new Emitter(); constructor(private contextService: IWorkspaceContextService) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders .map(folder => new ExplorerItem(folder.uri, undefined, true, false, false, folder.name)); - this._listener = this.contextService.onDidChangeWorkspaceFolders(() => setRoots()); setRoots(); + + this._listener = this.contextService.onDidChangeWorkspaceFolders(() => { + setRoots(); + this._onDidChangeRoots.fire(); + }); } get roots(): ExplorerItem[] { return this._roots; } + get onDidChangeRoots(): Event { + return this._onDidChangeRoots.event; + } + /** * Returns an array of child stat from this stat that matches with the provided path. * Starts matching from the first root. diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index fa8e44ea004..f215333699d 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -42,6 +42,7 @@ export interface IExplorerService { _serviceBrand: any; readonly roots: ExplorerItem[]; readonly sortOrder: SortOrder; + readonly onDidChangeRoots: Event; readonly onDidChangeItem: Event; readonly onDidChangeEditable: Event; readonly onDidSelectItem: Event<{ item: ExplorerItem, reveal: boolean }>; diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 3df4b26d8d1..9af0b4eb53b 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -30,6 +30,7 @@ export class ExplorerService implements IExplorerService { private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first + private _onDidChangeRoots = new Emitter(); private _onDidChangeItem = new Emitter(); private _onDidChangeEditable = new Emitter(); private _onDidSelectItem = new Emitter<{ item: ExplorerItem, reveal: boolean }>(); @@ -49,6 +50,10 @@ export class ExplorerService implements IExplorerService { return this.model.roots; } + get onDidChangeRoots(): Event { + return this._onDidChangeRoots.event; + } + get onDidChangeItem(): Event { return this._onDidChangeItem.event; } @@ -84,6 +89,7 @@ export class ExplorerService implements IExplorerService { this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this._onDidChangeItem.fire(null))); + this.disposables.push(model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); return model; } @@ -298,8 +304,11 @@ export class ExplorerService implements IExplorerService { private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void { const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default'; if (this._sortOrder !== configSortOrder) { + const shouldFire = this._sortOrder !== undefined; this._sortOrder = configSortOrder; - this.roots.forEach(r => this._onDidChangeItem.fire(r)); + if (shouldFire) { + this._onDidChangeRoots.fire(); + } } } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 8f2efd87c51..febda637939 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -17,7 +17,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CollapseAction2 } from 'vs/workbench/browser/viewlet'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { ExplorerDecorationsProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider'; -import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -150,13 +150,13 @@ export class ExplorerView extends ViewletPanel { this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); } - this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.setTreeInput(e.added))); this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.setTreeInput())); this.disposables.push(this.labelService.onDidRegisterFormatter(() => { this._onDidChangeTitleArea.fire(); this.refresh(); })); + this.disposables.push(this.explorerService.onDidChangeRoots(() => this.setTreeInput())); this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e))); this.disposables.push(this.explorerService.onDidChangeEditable(e => this.refresh(e.parent))); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); @@ -350,13 +350,16 @@ export class ExplorerView extends ViewletPanel { return DOM.getLargestChildWidth(parentNode, childNodes); } - private setTreeInput(newRoots?: IWorkspaceFolder[]): Promise { + private setTreeInput(): Promise { if (!this.isVisible()) { this.shouldRefresh = true; return Promise.resolve(void 0); } - perf.mark('willResolveExplorer'); + const initialInputSetup = !this.tree.getInput(); + if (initialInputSetup) { + perf.mark('willResolveExplorer'); + } const roots = this.explorerService.roots; let input: ExplorerItem | ExplorerItem[] = roots[0]; if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) { @@ -365,21 +368,20 @@ export class ExplorerView extends ViewletPanel { } const promise = this.tree.setInput(input).then(() => { - let expandPromise = Promise.resolve(void 0); - if (newRoots && newRoots.length) { - expandPromise = Promise.all(newRoots.map(workspaceFolder => this.tree.expand(this.explorerService.findClosest(workspaceFolder.uri)))); - } - // Find resource to focus from active editor input if set - if (this.autoReveal) { + if (this.autoReveal && initialInputSetup) { const resourceToFocus = this.getActiveFile(); if (resourceToFocus) { - return expandPromise.then(() => this.explorerService.select(resourceToFocus, true)); + return this.explorerService.select(resourceToFocus, true); } } - return expandPromise; - }).then(() => perf.mark('didResolveExplorer')); + return undefined; + }).then(() => { + if (initialInputSetup) { + perf.mark('didResolveExplorer'); + } + }); this.progressService.showWhile(promise, this.partService.isRestored() ? 800 : 1200 /* less ugly initial startup */); return promise; From f4713f023bc49323aea4dc84b097927f60f62cd9 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 17:26:21 +0100 Subject: [PATCH 35/65] explorer: make sure newly created folder gets selected --- src/vs/workbench/parts/files/electron-browser/fileActions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index aa5e42545b2..e04d0bb6edc 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -336,6 +336,7 @@ class CreateFolderAction extends BaseRenameAction { element: ExplorerItem, @IFileService private fileService: IFileService, @INotificationService notificationService: INotificationService, + @IExplorerService private explorerService: IExplorerService ) { super(CreateFolderAction.ID, CreateFolderAction.LABEL, element, notificationService); } @@ -343,7 +344,8 @@ class CreateFolderAction extends BaseRenameAction { public runAction(fileName: string): Promise { const resource = this.element.parent.resource; this.element.parent.removeChild(this.element); - return this.fileService.createFolder(resources.joinPath(resource, fileName)).then(void 0, (error) => { + const newResource = resources.joinPath(resource, fileName); + return this.fileService.createFolder(newResource).then(() => this.explorerService.select(newResource, true), (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); }); } From 0fdd5ff0a06ec81f5e6ff9a7536bb1e30afbafee Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 Dec 2018 17:29:21 +0100 Subject: [PATCH 36/65] isVisible -> isBodyVisible --- .../parts/files/electron-browser/views/explorerView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 87603f7e623..9c81f739060 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -333,7 +333,7 @@ export class ExplorerView extends ViewletPanel { * Refresh the contents of the explorer to get up to date data from the disk about the file structure. */ refresh(item?: ExplorerItem): Promise { - if (!this.tree || !this.isVisible()) { + if (!this.tree || !this.isBodyVisible()) { this.shouldRefresh = true; return Promise.resolve(void 0); } @@ -350,7 +350,7 @@ export class ExplorerView extends ViewletPanel { } private setTreeInput(): Promise { - if (!this.isVisible()) { + if (!this.isBodyVisible()) { this.shouldRefresh = true; return Promise.resolve(void 0); } @@ -399,7 +399,7 @@ export class ExplorerView extends ViewletPanel { } private onSelectItem(fileStat: ExplorerItem, reveal = this.autoReveal): Promise { - if (!fileStat || !this.isVisible()) { + if (!fileStat || !this.isBodyVisible()) { return Promise.resolve(void 0); } From 627ab44080253f7dce5e4972d4398b1aa5da4618 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 11:14:23 +0100 Subject: [PATCH 37/65] explorer: simplify actions and fix input box --- src/vs/workbench/parts/files/common/files.ts | 3 +- .../files/electron-browser/fileActions.ts | 324 ++++++------------ .../electron-browser/views/explorerView.ts | 8 +- .../electron-browser/views/explorerViewer.ts | 83 +---- 4 files changed, 119 insertions(+), 299 deletions(-) diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index f215333699d..ee25b7cea77 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -21,7 +21,6 @@ import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewCon import { Schemas } from 'vs/base/common/network'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; -import { IAction } from 'vs/base/common/actions'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; /** @@ -35,7 +34,7 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as string; - action: IAction; + onFinish: (value: string, success: boolean) => void; } export interface IExplorerService { diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 596994384c1..4bbbb5e8804 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -13,8 +13,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; -import { Action, IAction } from 'vs/base/common/actions'; -import { IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Action } from 'vs/base/common/actions'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; @@ -46,11 +45,7 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; - -export interface IEditableData { - action: IAction; - validator: IInputValidator; -} +import { onUnexpectedError } from 'vs/base/common/errors'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -100,145 +95,29 @@ export class BaseErrorReportingAction extends Action { } } -class TriggerRenameFileAction extends BaseErrorReportingAction { +const PLACEHOLDER_URI = URI.file(''); - public static readonly ID = 'renameFile'; - - private renameAction: BaseRenameAction; +/* New File */ +export class NewFileAction extends BaseErrorReportingAction { + static readonly ID = 'workbench.files.action.createFileFromExplorer'; + static readonly LABEL = nls.localize('createNewFile', "New File"); constructor( private element: ExplorerItem, @INotificationService notificationService: INotificationService, - @IInstantiationService instantiationService: IInstantiationService, - @IExplorerService private explorerService: IExplorerService + @IExplorerService private explorerService: IExplorerService, + @IFileService private fileService: IFileService, + @IEditorService private editorService: IEditorService ) { - super(TriggerRenameFileAction.ID, TRIGGER_RENAME_LABEL, notificationService); - - this.element = element; - this.renameAction = instantiationService.createInstance(RenameFileAction, element); + super('explorer.newFile', NEW_FILE_LABEL, notificationService); + this.class = 'explorer-action new-file'; } - public validateFileName(name: string): string { - const names: string[] = coalesce(name.split(/[\\/]/)); - if (names.length > 1) { // error only occurs on multi-path - const comparer = isLinux ? strings.compare : strings.compareIgnoreCase; - if (comparer(names[0], this.element.name) === 0) { - return nls.localize('renameWhenSourcePathIsParentOfTargetError', "Please use the 'New Folder' or 'New File' command to add children to an existing folder"); - } - } - - return this.renameAction.validateFileName(this.element.parent, name); - } - - public run(): Promise { - this.explorerService.setEditable(this.element, { validationMessage: this.validateFileName, action: this.renameAction }); - return Promise.resolve(void 0); - } -} - -export abstract class BaseRenameAction extends BaseErrorReportingAction { - - constructor( - id: string, - label: string, - public element: ExplorerItem, - @INotificationService notificationService: INotificationService, - ) { - super(id, label, notificationService); - this.enabled = this.element && !this.element.isReadonly; - } - - public run(context?: any): Promise { - if (!context) { - return Promise.reject(new Error('No context provided to BaseRenameFileAction.')); - } - - let name = context.value; - if (!name) { - return Promise.reject(new Error('No new name provided to BaseRenameFileAction.')); - } - - // Automatically trim whitespaces and trailing dots to produce nice file names - name = getWellFormedFileName(name); - const existingName = getWellFormedFileName(this.element.name); - - // Return early if name is invalid or didn't change - if (name === existingName || this.validateFileName(this.element.parent, name)) { - return Promise.resolve(null); - } - - // Call function and Emit Event through viewer - const promise = this.runAction(name).then(void 0, (error: any) => { - this.onError(error); - }); - - return promise; - } - - public validateFileName(parent: ExplorerItem, name: string): string { - let source = this.element.name; - let target = name; - - if (!isLinux) { // allow rename of same file also when case differs (e.g. Game.js => game.js) - source = source.toLowerCase(); - target = target.toLowerCase(); - } - - if (getWellFormedFileName(source) === getWellFormedFileName(target)) { - return null; - } - - return validateFileName(parent, name); - } - - public abstract runAction(newName: string): Promise; -} - -class RenameFileAction extends BaseRenameAction { - - public static readonly ID = 'workbench.files.action.renameFile'; - - constructor( - element: ExplorerItem, - @INotificationService notificationService: INotificationService, - @ITextFileService private textFileService: ITextFileService - ) { - super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, notificationService); - } - - public runAction(newName: string): Promise { - const parentResource = this.element.parent.resource; - const targetResource = resources.joinPath(parentResource, newName); - - return this.textFileService.move(this.element.resource, targetResource); - } -} - -/* Base New File/Folder Action */ -export class BaseNewAction extends BaseErrorReportingAction { - private presetFolder: ExplorerItem; - static readonly PLACEHOLDER_URI = URI.file(''); - - constructor( - id: string, - label: string, - private isFile: boolean, - private renameAction: BaseRenameAction, - element: ExplorerItem, - @INotificationService notificationService: INotificationService, - @IExplorerService private explorerService: IExplorerService - ) { - super(id, label, notificationService); - - if (element) { - this.presetFolder = element.isDirectory ? element : element.parent; - } - } - - public run(): Promise { - - let folder = this.presetFolder; - if (!folder) { + run(): Promise { + let folder: ExplorerItem; + if (this.element) { + folder = this.element.isDirectory ? this.element : this.element.parent; + } else { folder = this.explorerService.roots[0]; } @@ -246,40 +125,83 @@ export class BaseNewAction extends BaseErrorReportingAction { return Promise.reject(new Error('Parent folder is readonly.')); } - const stat = new ExplorerItem(BaseNewAction.PLACEHOLDER_URI, folder, !this.isFile); + const stat = new ExplorerItem(PLACEHOLDER_URI, folder, false); folder.addChild(stat); - this.renameAction.element = stat; - this.explorerService.setEditable(stat, { action: this.renameAction, validationMessage: value => this.renameAction.validateFileName(folder, value) }); - return Promise.resolve(void 0); - } -} + const onSuccess = value => { + return this.fileService.createFile(resources.joinPath(folder.resource, value)).then(stat => { + return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); + }, (error) => { + this.onErrorWithRetry(error, () => onSuccess(value)); + }); + }; -/* New File */ -export class NewFileAction extends BaseNewAction { - constructor( - element: ExplorerItem, - @INotificationService notificationService: INotificationService, - @IInstantiationService instantiationService: IInstantiationService, - @IExplorerService explorerService: IExplorerService - ) { - super('explorer.newFile', NEW_FILE_LABEL, true, instantiationService.createInstance(CreateFileAction, element), null, notificationService, explorerService); - this.class = 'explorer-action new-file'; + this.explorerService.setEditable(stat, { + validationMessage: value => validateFileName(stat, value), + onFinish: (value, success) => { + folder.removeChild(stat); + this.explorerService.setEditable(stat, null); + if (success) { + onSuccess(value); + } + } + }); + + return Promise.resolve(null); } } /* New Folder */ -export class NewFolderAction extends BaseNewAction { +export class NewFolderAction extends BaseErrorReportingAction { + static readonly ID = 'workbench.files.action.createFolderFromExplorer'; + static readonly LABEL = nls.localize('createNewFolder', "New Folder"); constructor( - element: ExplorerItem, + private element: ExplorerItem, @INotificationService notificationService: INotificationService, - @IInstantiationService instantiationService: IInstantiationService, - @IExplorerService explorerService: IExplorerService + @IFileService private fileService: IFileService, + @IExplorerService private explorerService: IExplorerService ) { - super('explorer.newFolder', NEW_FOLDER_LABEL, false, instantiationService.createInstance(CreateFolderAction, element), null, notificationService, explorerService); + super('explorer.newFolder', NEW_FOLDER_LABEL, notificationService); this.class = 'explorer-action new-folder'; } + + run(): Promise { + let folder: ExplorerItem; + if (this.element) { + folder = this.element.isDirectory ? this.element : this.element.parent; + } else { + folder = this.explorerService.roots[0]; + } + + if (folder.isReadonly) { + return Promise.reject(new Error('Parent folder is readonly.')); + } + + const stat = new ExplorerItem(PLACEHOLDER_URI, folder, true); + folder.addChild(stat); + + const onSuccess = value => { + return this.fileService.createFolder(resources.joinPath(folder.resource, value)).then(stat => { + return this.explorerService.select(stat.resource, true); + }, (error) => { + this.onErrorWithRetry(error, () => onSuccess(value)); + }); + }; + + this.explorerService.setEditable(stat, { + validationMessage: value => validateFileName(stat, value), + onFinish: (value, success) => { + folder.removeChild(stat); + this.explorerService.setEditable(stat, null); + if (success) { + onSuccess(value); + } + } + }); + + return Promise.resolve(null); + } } /* Create new file from anywhere: Open untitled */ @@ -300,57 +222,6 @@ export class GlobalNewUntitledFileAction extends Action { } } -/* Create New File (only used internally by explorerViewer) */ -class CreateFileAction extends BaseRenameAction { - - public static readonly ID = 'workbench.files.action.createFileFromExplorer'; - public static readonly LABEL = nls.localize('createNewFile', "New File"); - - constructor( - element: ExplorerItem, - @IEditorService private editorService: IEditorService, - @IFileService private fileService: IFileService, - @INotificationService notificationService: INotificationService, - ) { - super(CreateFileAction.ID, CreateFileAction.LABEL, element, notificationService); - } - - public runAction(fileName: string): Promise { - const resource = this.element.parent.resource; - this.element.parent.removeChild(this.element); - return this.fileService.createFile(resources.joinPath(resource, fileName)).then(stat => { - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); - }, (error) => { - this.onErrorWithRetry(error, () => this.runAction(fileName)); - }); - } -} - -/* Create New Folder (only used internally by explorerViewer) */ -class CreateFolderAction extends BaseRenameAction { - - public static readonly ID = 'workbench.files.action.createFolderFromExplorer'; - public static readonly LABEL = nls.localize('createNewFolder', "New Folder"); - - constructor( - element: ExplorerItem, - @IFileService private fileService: IFileService, - @INotificationService notificationService: INotificationService, - @IExplorerService private explorerService: IExplorerService - ) { - super(CreateFolderAction.ID, CreateFolderAction.LABEL, element, notificationService); - } - - public runAction(fileName: string): Promise { - const resource = this.element.parent.resource; - this.element.parent.removeChild(this.element); - const newResource = resources.joinPath(resource, fileName); - return this.fileService.createFolder(newResource).then(() => this.explorerService.select(newResource, true), (error) => { - this.onErrorWithRetry(error, () => this.runAction(fileName)); - }); - } -} - class BaseDeleteFileAction extends BaseErrorReportingAction { private static readonly CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; @@ -563,7 +434,6 @@ class BaseDeleteFileAction extends BaseErrorReportingAction { /* Add File */ export class AddFilesAction extends BaseErrorReportingAction { - constructor( private element: ExplorerItem, clazz: string, @@ -1125,8 +995,7 @@ export class ShowOpenedFileInNewWindow extends Action { } } -export function validateFileName(parent: ExplorerItem, name: string): string { - +export function validateFileName(item: ExplorerItem, name: string): string { // Produce a well formed file name name = getWellFormedFileName(name); @@ -1141,11 +1010,14 @@ export function validateFileName(parent: ExplorerItem, name: string): string { } const names = coalesce(name.split(/[\\/]/)); + const parent = item.parent; - // Do not allow to overwrite existing file - const childExists = !!parent.getChild(name); - if (childExists) { - return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); + if (name !== item.name) { + // Do not allow to overwrite existing file + const childExists = !!parent.getChild(name); + if (childExists) { + return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); + } } // Invalid File name @@ -1309,12 +1181,22 @@ CommandsRegistry.registerCommand({ }); export const renameHandler = (accessor: ServicesAccessor) => { - const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); - const explorerContext = getContext(listService.lastFocusedList); + const explorerService = accessor.get(IExplorerService); + const textFileService = accessor.get(ITextFileService); + const { stat } = getContext(listService.lastFocusedList); - const renameAction = instantationService.createInstance(TriggerRenameFileAction, explorerContext.stat); - return renameAction.run(); + explorerService.setEditable(stat, { + validationMessage: value => validateFileName(stat, value), + onFinish: (value, success) => { + if (success) { + const parentResource = stat.parent.resource; + const targetResource = resources.joinPath(parentResource, value); + textFileService.move(stat.resource, targetResource).then(undefined, onUnexpectedError); + } + explorerService.setEditable(stat, null); + } + }); }; export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 9c81f739060..bf19c48ec8e 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -158,7 +158,13 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.explorerService.onDidChangeRoots(() => this.setTreeInput())); this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e))); - this.disposables.push(this.explorerService.onDidChangeEditable(e => this.refresh(e.parent))); + this.disposables.push(this.explorerService.onDidChangeEditable(e => { + let expandPromise = Promise.resolve(null); + if (this.explorerService.getEditableData(e)) { + expandPromise = this.tree.expand(e.parent); + } + expandPromise.then(() => this.refresh(e.parent)); + })); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); // Update configuration diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index aa38bd07bc4..883dadb602d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -11,7 +11,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IFileService, FileKind } from 'vs/platform/files/common/files'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; @@ -19,18 +19,16 @@ import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfiguration, IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { dirname, joinPath, basename } from 'vs/base/common/resources'; +import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/parts/files/common/files'; +import { dirname, joinPath } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { once } from 'vs/base/common/functional'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { normalize, join, nativeSep } from 'vs/base/common/paths'; -import { rtrim } from 'vs/base/common/strings'; +import { normalize } from 'vs/base/common/paths'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; -import { IAction } from 'vs/base/common/actions'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; @@ -96,7 +94,6 @@ export class FilesRenderer implements ITreeRenderer(); @@ -148,7 +145,7 @@ export class FilesRenderer implements ITreeRenderer string, action: IAction }): void { + private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): void { // Use a file label only for the icon next to the input box const label = this.labels.create(container); @@ -191,18 +188,12 @@ export class FilesRenderer implements ITreeRenderer 0 && !stat.isDirectory ? lastDot : value.length }); inputBox.focus(); - const done = once(async (commit: boolean, blur: boolean) => { + const done = once(async (success: boolean, blur: boolean) => { label.element.style.display = 'none'; - - if (!commit) { - stat.parent.removeChild(stat); - } - if (commit && inputBox.value) { - await editableData.action.run({ value: inputBox.value }); - } + const value = inputBox.value; dispose(toDispose); container.removeChild(label.element); - this.explorerService.setEditable(stat, null); + editableData.onFinish(value, success); }); const toDispose = [ @@ -216,14 +207,6 @@ export class FilesRenderer implements ITreeRenderer { - const initialRelPath: string = path.relative(stat.root.resource.path, stat.parent.resource.path); - let projectFolderName: string = ''; - if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - projectFolderName = basename(stat.root.resource); // show root folder name in multi-folder project - } - this.showInputMessage(inputBox, initialRelPath, projectFolderName, editableData.action.id); - }), DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { done(inputBox.isInputValid(), true); }), @@ -232,47 +215,6 @@ export class FilesRenderer implements ITreeRenderer, index: number, templateData: IFileTemplateData): void { // noop } @@ -419,15 +361,6 @@ export class FileSorter implements ITreeSorter { break; } - // Sort "New File/Folder" placeholders - if (this.explorerService.getEditableData(statA)) { - return -1; - } - - if (this.explorerService.getEditableData(statB)) { - return 1; - } - // Sort Files switch (sortOrder) { case 'type': From 35a5ebdf32a09044694ecb521b1fcb3e36f10fbc Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 11:27:26 +0100 Subject: [PATCH 38/65] explorer: do not cache children, tree will do that for you --- .../workbench/parts/files/common/explorerModel.ts | 14 ++++---------- .../files/electron-browser/views/explorerView.ts | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index e5a7487021e..9b90f950648 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -247,16 +247,10 @@ export class ExplorerItem { } fetchChildren(fileService: IFileService): Promise { - let promise = Promise.resolve(null); - if (!this.isDirectoryResolved) { - promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { - const resolved = ExplorerItem.create(stat, this); - ExplorerItem.mergeLocalWithDisk(resolved, this); - this.isDirectoryResolved = true; - }); - } - - return promise.then(() => { + return fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { + const resolved = ExplorerItem.create(stat, this); + ExplorerItem.mergeLocalWithDisk(resolved, this); + this.isDirectoryResolved = true; const items: ExplorerItem[] = []; this.children.forEach(child => { items.push(child); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index bf19c48ec8e..aa6bb922629 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -161,7 +161,7 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.explorerService.onDidChangeEditable(e => { let expandPromise = Promise.resolve(null); if (this.explorerService.getEditableData(e)) { - expandPromise = this.tree.expand(e.parent); + expandPromise = ignoreErrors(this.tree.expand(e.parent)); } expandPromise.then(() => this.refresh(e.parent)); })); From 8151464a404796794f94aac40b5646b73ea45d22 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 11:34:53 +0100 Subject: [PATCH 39/65] fix multi selection context computation to work with new tree --- src/vs/workbench/parts/files/browser/files.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index 93bf737781c..f745fc4a024 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { IListService } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { OpenEditor } from 'vs/workbench/parts/files/common/files'; import { toResource } from 'vs/workbench/common/editor'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; @@ -45,7 +44,7 @@ export function getMultiSelectedResources(resource: URI | object, listService: I const list = listService.lastFocusedList; if (list && list.getHTMLElement() === document.activeElement) { // Explorer - if (list instanceof Tree) { + if (list instanceof WorkbenchAsyncDataTree) { const selection = list.getSelection().map((fs: ExplorerItem) => fs.resource); const focus = list.getFocus(); const mainUriStr = URI.isUri(resource) ? resource.toString() : focus instanceof ExplorerItem ? focus.resource.toString() : undefined; From dacc2355d2bab35f21791efdcea227f0d0a3a824 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 11:48:47 +0100 Subject: [PATCH 40/65] explorer: ignore active editor change events produced by explorer --- .../parts/files/electron-browser/views/explorerView.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index aa6bb922629..85f54821f5e 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -59,6 +59,7 @@ export class ExplorerView extends ViewletPanel { private dragHandler: DelayedDragHandler; private decorationProvider: ExplorerDecorationsProvider; private autoReveal = false; + private ignoreActiveEditorChange; constructor( options: IViewletPanelOptions, @@ -173,7 +174,7 @@ export class ExplorerView extends ViewletPanel { // When the explorer viewer is loaded, listen to changes to the editor input this.disposables.push(this.editorService.onDidActiveEditorChange(() => { - if (this.autoReveal) { + if (this.autoReveal && !this.ignoreActiveEditorChange) { const activeFile = this.getActiveFile(); if (activeFile) { this.explorerService.select(this.getActiveFile()); @@ -278,7 +279,12 @@ export class ExplorerView extends ViewletPanel { "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } }*/ this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: (e.browserEvent instanceof MouseEvent) && !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + this.ignoreActiveEditorChange = true; + this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: (e.browserEvent instanceof MouseEvent) && !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP) + .then(() => this.ignoreActiveEditorChange = false).catch(e => { + this.ignoreActiveEditorChange = false; + onUnexpectedError(e); + }); } } })); From 41b0bf63bea9503c1fd2d98abbed7e673fa6f9a6 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 12:17:10 +0100 Subject: [PATCH 41/65] explorer: smarter resolving of children stats with cache --- .../workbench/parts/files/common/explorerModel.ts | 14 ++++++++++---- .../files/electron-browser/explorerService.ts | 10 +++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 9b90f950648..e5a7487021e 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -247,10 +247,16 @@ export class ExplorerItem { } fetchChildren(fileService: IFileService): Promise { - return fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { - const resolved = ExplorerItem.create(stat, this); - ExplorerItem.mergeLocalWithDisk(resolved, this); - this.isDirectoryResolved = true; + let promise = Promise.resolve(null); + if (!this.isDirectoryResolved) { + promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { + const resolved = ExplorerItem.create(stat, this); + ExplorerItem.mergeLocalWithDisk(resolved, this); + this.isDirectoryResolved = true; + }); + } + + return promise.then(() => { const items: ExplorerItem[] = []; this.children.forEach(child => { items.push(child); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 9af0b4eb53b..765d8573a5f 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -220,6 +220,10 @@ export class ExplorerService implements IExplorerService { setTimeout(() => { // Filter to the ones we care e = this.filterToViewRelevantEvents(e); + const explorerItemChanged = (item: ExplorerItem) => { + item.isDirectoryResolved = false; + this._onDidChangeItem.fire(item); + }; // Handle added files/folders const added = e.getAdded(); @@ -241,7 +245,7 @@ export class ExplorerService implements IExplorerService { // Compute if parent is visible and added file not yet part of it const parentStat = this.model.findClosest(parent); if (parentStat && parentStat.isDirectoryResolved && !this.model.findClosest(change.resource)) { - this._onDidChangeItem.fire(parentStat); + explorerItemChanged(parentStat); } // Keep track of path that can be ignored for faster lookup @@ -260,7 +264,7 @@ export class ExplorerService implements IExplorerService { const del = deleted[j]; const item = this.model.findClosest(del.resource); if (item) { - this._onDidChangeItem.fire(item.parent); + explorerItemChanged(item.parent); } } } @@ -275,7 +279,7 @@ export class ExplorerService implements IExplorerService { const item = this.model.findClosest(upd.resource); if (item) { - this._onDidChangeItem.fire(item.parent); + explorerItemChanged(item.parent); } } } From 834eec91f03864054492ccd2f2bbf8c6e9932c7c Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 Dec 2018 12:40:07 +0100 Subject: [PATCH 42/65] explorer: error decorations for broken root --- src/vs/workbench/parts/files/common/explorerModel.ts | 12 ++++-------- .../parts/files/electron-browser/explorerService.ts | 11 ++++++----- .../views/explorerDecorationsProvider.ts | 3 +++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index e5a7487021e..36217b1deb4 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -74,6 +74,7 @@ export class ExplorerModel implements IDisposable { export class ExplorerItem { public isDirectoryResolved: boolean; + public isError: boolean; constructor( public resource: URI, @@ -84,7 +85,6 @@ export class ExplorerItem { private _name: string = resources.basenameOrAuthority(resource), private _mtime?: number, private _etag?: string, - private _isError?: boolean ) { this.isDirectoryResolved = false; } @@ -109,10 +109,6 @@ export class ExplorerItem { return this._mtime; } - get isError(): boolean { - return !!this._isError; - } - get name(): string { return this._name; } @@ -152,8 +148,8 @@ export class ExplorerItem { return this === this.root; } - static create(raw: IFileStat, parent: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { - const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, raw.isReadonly, raw.name, raw.mtime, raw.etag, isError); + static create(raw: IFileStat, parent: ExplorerItem, resolveTo?: URI[]): ExplorerItem { + const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, raw.isReadonly, raw.name, raw.mtime, raw.etag); // Recursively add children if present if (stat.isDirectory) { @@ -201,7 +197,7 @@ export class ExplorerItem { local.isDirectoryResolved = disk.isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; local._isReadonly = disk.isReadonly; - local._isError = disk.isError; + local.isError = disk.isError; // Merge Children if resolved if (mergingDirectories && disk.isDirectoryResolved) { diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index 765d8573a5f..e459bba9f83 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -16,7 +16,6 @@ import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IExpression } from 'vs/base/common/glob'; -import { INotificationService } from 'vs/platform/notification/common/notification'; function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): IExpression { const scope = root ? { resource: root } : void 0; @@ -42,8 +41,7 @@ export class ExplorerService implements IExplorerService { @IFileService private fileService: IFileService, @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @INotificationService private notificationService: INotificationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService ) { } get roots(): ExplorerItem[] { @@ -120,17 +118,20 @@ export class ExplorerService implements IExplorerService { const options: IResolveFileOptions = { resolveTo: [resource] }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; + const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); return this.fileService.resolveFile(rootUri, options).then(stat => { // Convert to model - const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop(); const modelStat = ExplorerItem.create(stat, null, options.resolveTo); // Update Input with disk Stat ExplorerItem.mergeLocalWithDisk(modelStat, root); // Select and Reveal this._onDidSelectItem.fire({ item: root.find(resource), reveal }); - }, e => { this.notificationService.error(e); }); + }, () => { + root.isError = true; + this._onDidChangeItem.fire(root); + }); } // File events diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts index e4fc759afaa..402ae0ec2a5 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts @@ -26,6 +26,9 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { this.toDispose.push(contextService.onDidChangeWorkspaceFolders(e => { this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri)); })); + this.toDispose.push(explorerService.onDidChangeItem(item => { + this._onDidChange.fire([item.resource]); + })); } get onDidChange(): Event { From 00e4a138708feaf22301c5ffea9871c1fa48eef8 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 27 Dec 2018 16:46:03 +0100 Subject: [PATCH 43/65] explorerView: multipleSelectionSupport --- .../workbench/parts/files/electron-browser/views/explorerView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 85f54821f5e..3645830b320 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -232,6 +232,7 @@ export class ExplorerView extends ViewletPanel { keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: stat => stat.name }, + multipleSelectionSupport: true, filter: this.filter, sorter: this.instantiationService.createInstance(FileSorter) }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); From 5b06dcc20c7f13bad76a6c98a6d89461b4a002de Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 27 Dec 2018 16:54:33 +0100 Subject: [PATCH 44/65] fix strick null errors --- .../parts/files/common/explorerModel.ts | 28 +++++++++++-------- .../views/explorerDecorationsProvider.ts | 4 ++- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 36217b1deb4..e0915b71044 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -78,7 +78,7 @@ export class ExplorerItem { constructor( public resource: URI, - private _parent: ExplorerItem, + private _parent: ExplorerItem | undefined, private _isDirectory?: boolean, private _isSymbolicLink?: boolean, private _isReadonly?: boolean, @@ -101,11 +101,11 @@ export class ExplorerItem { return !!this._isReadonly; } - get etag(): string { + get etag(): string | undefined { return this._etag; } - get mtime(): number { + get mtime(): number | undefined { return this._mtime; } @@ -113,7 +113,7 @@ export class ExplorerItem { return this._name; } - get parent(): ExplorerItem { + get parent(): ExplorerItem | undefined { return this._parent; } @@ -122,7 +122,7 @@ export class ExplorerItem { return this; } - return this.parent.root; + return this._parent.root; } @memoize get children(): Map { @@ -131,12 +131,12 @@ export class ExplorerItem { private updateName(value: string): void { // Re-add to parent since the parent has a name map to children and the name might have changed - if (this.parent) { - this.parent.removeChild(this); + if (this._parent) { + this._parent.removeChild(this); } this._name = value; - if (this.parent) { - this.parent.addChild(this); + if (this._parent) { + this._parent.addChild(this); } } @@ -243,7 +243,7 @@ export class ExplorerItem { } fetchChildren(fileService: IFileService): Promise { - let promise = Promise.resolve(null); + let promise: Promise = Promise.resolve(undefined); if (!this.isDirectoryResolved) { promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { const resolved = ExplorerItem.create(stat, this); @@ -277,14 +277,18 @@ export class ExplorerItem { * Moves this element under a new parent element. */ move(newParent: ExplorerItem): void { - this.parent.removeChild(this); + if (this._parent) { + this._parent.removeChild(this); + } newParent.removeChild(this); // make sure to remove any previous version of the file if any newParent.addChild(this); this.updateResource(true); } private updateResource(recursive: boolean): void { - this.resource = resources.joinPath(this.parent.resource, this.name); + if (this._parent) { + this.resource = resources.joinPath(this._parent.resource, this.name); + } if (recursive) { if (this.isDirectory) { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts index 402ae0ec2a5..46cc8f53195 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts @@ -27,7 +27,9 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri)); })); this.toDispose.push(explorerService.onDidChangeItem(item => { - this._onDidChange.fire([item.resource]); + if (item) { + this._onDidChange.fire([item.resource]); + } })); } From 8205d0b8b2114d89a4c4f3910d3967ddbc9e196d Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 28 Dec 2018 15:44:57 +0100 Subject: [PATCH 45/65] fix unit tests --- .../parts/files/electron-browser/fileActions.ts | 2 +- .../test/electron-browser/explorerModel.test.ts | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 4bbbb5e8804..12a1f4792f0 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -1014,7 +1014,7 @@ export function validateFileName(item: ExplorerItem, name: string): string { if (name !== item.name) { // Do not allow to overwrite existing file - const childExists = !!parent.getChild(name); + const childExists = parent && !!parent.getChild(name); if (childExists) { return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); } diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index e37d12d7ce6..a5f20068a38 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -202,8 +202,7 @@ suite('Files - View Model', () => { assert(validateFileName(s, 'foo>bar') !== null); assert(validateFileName(s, 'foo|bar') !== null); } - assert(validateFileName(s, 'alles.klar') !== null); - + assert(validateFileName(s, 'alles.klar') === null); assert(validateFileName(s, '.foo') === null); assert(validateFileName(s, 'foo.bar') === null); assert(validateFileName(s, 'foo') === null); @@ -215,15 +214,10 @@ suite('Files - View Model', () => { const sChild = createStat('/path/to/stat/alles.klar', 'alles.klar', true, true, 8096, d); s.addChild(sChild); - assert(validateFileName(s, 'alles.klar') !== null); + assert(validateFileName(s, 'alles.klar') === null); - if (isLinux) { - assert(validateFileName(s, 'Alles.klar') === null); - assert(validateFileName(s, 'Alles.Klar') === null); - } else { - assert(validateFileName(s, 'Alles.klar') !== null); - assert(validateFileName(s, 'Alles.Klar') !== null); - } + assert(validateFileName(s, 'Alles.klar') === null); + assert(validateFileName(s, 'Alles.Klar') === null); assert(validateFileName(s, '.foo') === null); assert(validateFileName(s, 'foo.bar') === null); From 39d179920bd503a445c57791705e71d7dd6339c4 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 3 Jan 2019 15:42:28 +0100 Subject: [PATCH 46/65] explorer: polish rename box, draw highlight --- .../parts/files/electron-browser/media/explorerviewlet.css | 5 +++++ .../parts/files/electron-browser/views/explorerView.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index 612f6733d86..a38a2026ee5 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -14,6 +14,11 @@ height: 100%; } +.explorer-viewlet .monaco-list.highlight .explorer-item:not(.explorer-item-edited), +.explorer-viewlet .monaco-list.highlight .monaco-tl-twistie { + opacity: 0.3; +} + .explorer-viewlet .explorer-item, .explorer-viewlet .open-editor, .explorer-viewlet .editor-group { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 3645830b320..2627a640f12 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -161,9 +161,12 @@ export class ExplorerView extends ViewletPanel { this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e))); this.disposables.push(this.explorerService.onDidChangeEditable(e => { let expandPromise = Promise.resolve(null); - if (this.explorerService.getEditableData(e)) { + const isEditing = !!this.explorerService.getEditableData(e); + if (isEditing) { + this.tree.setFocus([]); expandPromise = ignoreErrors(this.tree.expand(e.parent)); } + DOM.toggleClass(this.tree.getHTMLElement(), 'highlight', isEditing); expandPromise.then(() => this.refresh(e.parent)); })); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); From 5325e2bfe6180a53c764b7fb4027c792db6cbb4a Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 3 Jan 2019 15:56:46 +0100 Subject: [PATCH 47/65] explorer: do not react if user is clicking on directories or explorer items which are input placeholders --- .../electron-browser/views/explorerView.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 2627a640f12..790b3096df9 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -265,6 +265,10 @@ export class ExplorerView extends ViewletPanel { const selection = e.elements; // Do not react if the user is expanding selection if (selection && selection.length === 1) { + if (selection[0].isDirectory || !selection[0].name) { + // Do not react if user is clicking on directories or explorer items which are input placeholders + return; + } let isDoubleClick = false; let sideBySide = false; let isMiddleClick = false; @@ -275,21 +279,19 @@ export class ExplorerView extends ViewletPanel { sideBySide = this.tree.useAltAsMultipleSelectionModifier ? (e.browserEvent.ctrlKey || e.browserEvent.metaKey) : e.browserEvent.altKey; } - if (!selection[0].isDirectory) { - // Pass focus for keyboard events and for double click - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - }*/ - this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); - this.ignoreActiveEditorChange = true; - this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: (e.browserEvent instanceof MouseEvent) && !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP) - .then(() => this.ignoreActiveEditorChange = false).catch(e => { - this.ignoreActiveEditorChange = false; - onUnexpectedError(e); - }); - } + // Pass focus for keyboard events and for double click + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + }*/ + this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' }); + this.ignoreActiveEditorChange = true; + this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: (e.browserEvent instanceof MouseEvent) && !isDoubleClick, pinned: isDoubleClick || isMiddleClick } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP) + .then(() => this.ignoreActiveEditorChange = false).catch(e => { + this.ignoreActiveEditorChange = false; + onUnexpectedError(e); + }); } })); From a7e590a4a667890f8a1bff5ce22d253a8d40a522 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 7 Jan 2019 15:18:51 +0100 Subject: [PATCH 48/65] fix file tests on windows --- src/vs/workbench/parts/files/electron-browser/fileActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 08b69e5d866..097b9ba355b 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -1027,7 +1027,7 @@ export function validateFileName(item: ExplorerItem, name: string): string { // Max length restriction (on Windows) if (isWindows) { - const fullPathLength = name.length + parent.resource.fsPath.length + 1 /* path segment */; + const fullPathLength = item.resource.fsPath.length + 1 /* path segment */; if (fullPathLength > 255) { return nls.localize('filePathTooLongError', "The name **{0}** results in a path that is too long. Please choose a shorter name.", trimLongName(name)); } From 483fce634ff2e136438f227add693ec6d4763591 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 10 Jan 2019 15:41:59 +0100 Subject: [PATCH 49/65] explorer drag and drop --- .../files/electron-browser/fileActions.ts | 110 +-- .../electron-browser/views/explorerView.ts | 5 +- .../electron-browser/views/explorerViewer.ts | 778 ++++++++++-------- 3 files changed, 438 insertions(+), 455 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 097b9ba355b..db6b2c76bcf 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/fileactions'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { isWindows, isLinux } from 'vs/base/common/platform'; -import { sequence, ITask, always } from 'vs/base/common/async'; +import { always } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -16,8 +16,8 @@ import * as strings from 'vs/base/common/strings'; import { Action } from 'vs/base/common/actions'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService } from 'vs/workbench/parts/files/common/files'; -import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { ExplorerViewlet } from 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -36,7 +36,7 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c import { IListService, ListWidget } from 'vs/platform/list/browser/listService'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Schemas } from 'vs/base/common/network'; -import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IConfirmationResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Constants } from 'vs/editor/common/core/uint'; @@ -431,106 +431,6 @@ class BaseDeleteFileAction extends BaseErrorReportingAction { } } -/* Add File */ -export class AddFilesAction extends BaseErrorReportingAction { - - constructor( - private element: ExplorerItem, - clazz: string, - @IFileService private readonly fileService: IFileService, - @IEditorService private readonly editorService: IEditorService, - @IDialogService private readonly dialogService: IDialogService, - @INotificationService notificationService: INotificationService, - @ITextFileService private readonly textFileService: ITextFileService, - @IExplorerService private readonly explorerService: IExplorerService - ) { - super('workbench.files.action.addFile', nls.localize('addFiles', "Add Files"), notificationService); - - if (clazz) { - this.class = clazz; - } - } - - public run(resourcesToAdd: URI[]): Promise { - if (resourcesToAdd && resourcesToAdd.length > 0) { - - // Find parent to add to - let targetElement: ExplorerItem; - if (this.element) { - targetElement = this.element; - } else { - targetElement = this.explorerService.roots[0]; - } - - if (!targetElement.isDirectory) { - targetElement = targetElement.parent; - } - - // Resolve target to check for name collisions and ask user - return this.fileService.resolveFile(targetElement.resource).then((targetStat: IFileStat) => { - - // Check for name collisions - const targetNames = new Set(); - targetStat.children.forEach((child) => { - targetNames.add(isLinux ? child.name : child.name.toLowerCase()); - }); - - let overwritePromise: Promise = Promise.resolve({ confirmed: true }); - if (resourcesToAdd.some(resource => { - return targetNames.has(!resources.hasToIgnoreCase(resource) ? resources.basename(resource) : resources.basename(resource).toLowerCase()); - })) { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - overwritePromise = this.dialogService.confirm(confirm); - } - - return overwritePromise.then(res => { - if (!res.confirmed) { - return undefined; - } - - // Run add in sequence - const addPromisesFactory: ITask>[] = []; - resourcesToAdd.forEach(resource => { - addPromisesFactory.push(() => { - const sourceFile = resource; - const targetFile = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); - - // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents - // of the target file would replace the contents of the added file. since we already - // confirmed the overwrite before, this is OK. - let revertPromise: Promise = Promise.resolve(null); - if (this.textFileService.isDirty(targetFile)) { - revertPromise = this.textFileService.revertAll([targetFile], { soft: true }); - } - - return revertPromise.then(() => { - const target = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); - return this.fileService.copyFile(sourceFile, target, true).then(stat => { - - // if we only add one file, just open it directly - if (resourcesToAdd.length === 1) { - this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); - } - }, error => this.onError(error)); - }); - }); - }); - - return sequence(addPromisesFactory); - }); - }); - } - - return Promise.resolve(undefined); - } -} - // Copy File/Folder class CopyFileAction extends BaseErrorReportingAction { @@ -603,7 +503,7 @@ class PasteFileAction extends BaseErrorReportingAction { } } -function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean }): URI { +export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean }): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); let candidate = resources.joinPath(targetFolder.resource, name); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 72b5d85b424..e32a1a11cd2 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -31,7 +31,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -237,7 +237,8 @@ export class ExplorerView extends ViewletPanel { }, multipleSelectionSupport: true, filter: this.filter, - sorter: this.instantiationService.createInstance(FileSorter) + sorter: this.instantiationService.createInstance(FileSorter), + dnd: this.instantiationService.createInstance(FileDragAndDrop) }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.disposables.push(this.tree); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 77527fedd37..fec0dea1640 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -6,21 +6,21 @@ import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IFileService, FileKind } from 'vs/platform/files/common/files'; +import { IFileService, FileKind, IFileStat, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/parts/files/common/files'; -import { dirname, joinPath } from 'vs/base/common/resources'; +import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -31,6 +31,21 @@ import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'path'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; +import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from 'vs/workbench/browser/dnd'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; +import { Schemas } from 'vs/base/common/network'; +import { DesktopDragAndDropData, ExternalElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { isMacintosh, isLinux } from 'vs/base/common/platform'; +import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { URI } from 'vs/base/common/uri'; +import { ITask, sequence } from 'vs/base/common/async'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { findValidPasteFileTarget } from 'vs/workbench/parts/files/electron-browser/fileActions'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -379,345 +394,412 @@ export class FileSorter implements ITreeSorter { } } -// export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { - -// private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; - -// private toDispose: IDisposable[]; -// private dropEnabled: boolean; - -// constructor( -// @INotificationService private notificationService: INotificationService, -// @IDialogService private dialogService: IDialogService, -// @IWorkspaceContextService private contextService: IWorkspaceContextService, -// @IFileService private fileService: IFileService, -// @IConfigurationService private configurationService: IConfigurationService, -// @IInstantiationService instantiationService: IInstantiationService, -// @ITextFileService private textFileService: ITextFileService, -// @IWindowService private windowService: IWindowService, -// @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService -// ) { -// super(stat => this.statToResource(stat), instantiationService); - -// this.toDispose = []; - -// this.updateDropEnablement(); - -// this.registerListeners(); -// } - -// private statToResource(stat: ExplorerItem): URI { -// if (stat.isDirectory) { -// return URI.from({ scheme: 'folder', path: stat.resource.path }); // indicates that we are dragging a folder -// } - -// return stat.resource; -// } - -// private registerListeners(): void { -// this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateDropEnablement())); -// } - -// private updateDropEnablement(): void { -// this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop'); -// } - -// public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void { -// const sources: ExplorerItem[] = data.getData(); -// if (sources && sources.length) { - -// // When dragging folders, make sure to collapse them to free up some space -// sources.forEach(s => { -// if (s.isDirectory && tree.isExpanded(s)) { -// tree.collapse(s, false); -// } -// }); - -// // Apply some datatransfer types to allow for dragging the element outside of the application -// this.instantiationService.invokeFunction(fillResourceDataTransfers, sources, originalEvent); - -// // The only custom data transfer we set from the explorer is a file transfer -// // to be able to DND between multiple code file explorers across windows -// const fileResources = sources.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); -// if (fileResources.length) { -// originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources)); -// } -// } -// } - -// public onDragOver(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): IDragOverReaction { -// if (!this.dropEnabled) { -// return DRAG_OVER_REJECT; -// } - -// const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); -// const fromDesktop = data instanceof DesktopDragAndDropData; - -// // Desktop DND -// if (fromDesktop) { -// const types: string[] = originalEvent.dataTransfer.types; -// const typesArray: string[] = []; -// for (let i = 0; i < types.length; i++) { -// typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase -// } - -// if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) { -// return DRAG_OVER_REJECT; -// } -// } - -// // Other-Tree DND -// else if (data instanceof ExternalElementsDragAndDropData) { -// return DRAG_OVER_REJECT; -// } - -// // In-Explorer DND -// else { -// const sources: ExplorerItem[] = data.getData(); -// if (target instanceof Model) { -// if (sources[0].isRoot) { -// return DRAG_OVER_ACCEPT_BUBBLE_DOWN(false); -// } - -// return DRAG_OVER_REJECT; -// } - -// if (!Array.isArray(sources)) { -// return DRAG_OVER_REJECT; -// } - -// if (sources.some((source) => { -// if (source instanceof NewStatPlaceholder) { -// return true; // NewStatPlaceholders can not be moved -// } - -// if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) { -// return true; // Root folder can not be moved to a non root file stat. -// } - -// if (source.resource.toString() === target.resource.toString()) { -// return true; // Can not move anything onto itself -// } - -// if (source.isRoot && target instanceof ExplorerItem && target.isRoot) { -// // Disable moving workspace roots in one another -// return false; -// } - -// if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) { -// return true; // Can not move a file to the same parent unless we copy -// } - -// if (resources.isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) { -// return true; // Can not move a parent folder into one of its children -// } - -// return false; -// })) { -// return DRAG_OVER_REJECT; -// } -// } - -// // All (target = model) -// if (target instanceof Model) { -// return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(false) : DRAG_OVER_REJECT; // can only drop folders to workspace -// } - -// // All (target = file/folder) -// else { -// if (target.isDirectory) { -// if (target.isReadonly) { -// return DRAG_OVER_REJECT; -// } -// return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true); -// } - -// if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) { -// return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_UP_COPY : DRAG_OVER_ACCEPT_BUBBLE_UP; -// } -// } - -// return DRAG_OVER_REJECT; -// } - -// public drop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): void { - -// // Desktop DND (Import file) -// if (data instanceof DesktopDragAndDropData) { -// this.handleExternalDrop(tree, data, target, originalEvent); -// } - -// // In-Explorer DND (Move/Copy file) -// else { -// this.handleExplorerDrop(tree, data, target, originalEvent); -// } -// } - -// private handleExternalDrop(tree: ITree, data: DesktopDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): Promise { -// const droppedResources = extractResources(originalEvent.browserEvent as DragEvent, true); - -// // Check for dropped external files to be folders -// return this.fileService.resolveFiles(droppedResources).then(result => { - -// // Pass focus to window -// this.windowService.focusWindow(); - -// // Handle folders by adding to workspace if we are in workspace context -// const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource })); -// if (folders.length > 0) { - -// // If we are in no-workspace context, ask for confirmation to create a workspace -// let confirmedPromise: Promise = Promise.resolve({ confirmed: true }); -// if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) { -// confirmedPromise = this.dialogService.confirm({ -// message: folders.length > 1 ? nls.localize('dropFolders', "Do you want to add the folders to the workspace?") : nls.localize('dropFolder', "Do you want to add the folder to the workspace?"), -// type: 'question', -// primaryButton: folders.length > 1 ? nls.localize('addFolders', "&&Add Folders") : nls.localize('addFolder', "&&Add Folder") -// }); -// } - -// return confirmedPromise.then(res => { -// if (res.confirmed) { -// return this.workspaceEditingService.addFolders(folders); -// } - -// return undefined; -// }); -// } - -// // Handle dropped files (only support FileStat as target) -// else if (target instanceof ExplorerItem && !target.isReadonly) { -// const addFilesAction = this.instantiationService.createInstance(AddFilesAction, tree, target, null); - -// return addFilesAction.run(droppedResources.map(res => res.resource)); -// } - -// return undefined; -// }); -// } - -// private handleExplorerDrop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): Promise { -// const sources: ExplorerItem[] = resources.distinctParents(data.getData(), s => s.resource); -// const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); - -// let confirmPromise: Promise; - -// // Handle confirm setting -// const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); -// if (confirmDragAndDrop) { -// confirmPromise = this.dialogService.confirm({ -// message: sources.length > 1 && sources.every(s => s.isRoot) ? nls.localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") -// : sources.length > 1 ? getConfirmMessage(nls.localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", sources.length), sources.map(s => s.resource)) -// : sources[0].isRoot ? nls.localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", sources[0].name) -// : nls.localize('confirmMove', "Are you sure you want to move '{0}'?", sources[0].name), -// checkbox: { -// label: nls.localize('doNotAskAgain', "Do not ask me again") -// }, -// type: 'question', -// primaryButton: nls.localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") -// }); -// } else { -// confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult); -// } - -// return confirmPromise.then(res => { - -// // Check for confirmation checkbox -// let updateConfirmSettingsPromise: Promise = Promise.resolve(undefined); -// if (res.confirmed && res.checkboxChecked === true) { -// updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER); -// } - -// return updateConfirmSettingsPromise.then(() => { -// if (res.confirmed) { -// const rootDropPromise = this.doHandleRootDrop(sources.filter(s => s.isRoot), target); -// return Promise.all(sources.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(tree, source, target, isCopy)).concat(rootDropPromise)).then(() => undefined); -// } - -// return Promise.resolve(undefined); -// }); -// }); -// } - -// private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem | Model): Promise { -// if (roots.length === 0) { -// return Promise.resolve(undefined); -// } - -// const folders = this.contextService.getWorkspace().folders; -// let targetIndex: number; -// const workspaceCreationData: IWorkspaceFolderCreationData[] = []; -// const rootsToMove: IWorkspaceFolderCreationData[] = []; - -// for (let index = 0; index < folders.length; index++) { -// const data = { -// uri: folders[index].uri -// }; -// if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) { -// targetIndex = workspaceCreationData.length; -// } - -// if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) { -// workspaceCreationData.push(data); -// } else { -// rootsToMove.push(data); -// } -// } -// if (target instanceof Model) { -// targetIndex = workspaceCreationData.length; -// } - -// workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); -// return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); -// } - -// private doHandleExplorerDrop(tree: ITree, source: ExplorerItem, target: ExplorerItem | Model, isCopy: boolean): Promise { -// if (!(target instanceof ExplorerItem)) { -// return Promise.resolve(undefined); -// } - -// return tree.expand(target).then(() => { - -// if (target.isReadonly) { -// return undefined; -// } - -// // Reuse duplicate action if user copies -// if (isCopy) { -// return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run(); -// } - -// // Otherwise move -// const targetResource = resources.joinPath(target.resource, source.name); - -// return this.textFileService.move(source.resource, targetResource).then(undefined, error => { - -// // Conflict -// if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { -// const confirm: IConfirmation = { -// message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), -// detail: nls.localize('irreversible', "This action is irreversible!"), -// primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), -// type: 'warning' -// }; - -// // Move with overwrite if the user confirms -// return this.dialogService.confirm(confirm).then(res => { -// if (res.confirmed) { -// return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error)); -// } - -// return undefined; -// }); -// } - -// // Any other error -// else { -// this.notificationService.error(error); -// } - -// return undefined; -// }); -// }, errors.onUnexpectedError); -// } -// } +export class FileDragAndDrop implements ITreeDragAndDrop { + private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; + + private toDispose: IDisposable[]; + private dropEnabled: boolean; + + constructor( + @INotificationService private notificationService: INotificationService, + @IExplorerService private explorerService: IExplorerService, + @IEditorService private editorService: IEditorService, + @IDialogService private dialogService: IDialogService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IFileService private fileService: IFileService, + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService private instantiationService: IInstantiationService, + @ITextFileService private textFileService: ITextFileService, + @IWindowService private windowService: IWindowService, + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + ) { + this.toDispose = []; + + const updateDropEnablement = () => { + this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop'); + }; + updateDropEnablement(); + this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => updateDropEnablement())); + } + + onDragOver(data: IDragAndDropData, target: ExplorerItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + if (!this.dropEnabled) { + return false; + } + + const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); + const fromDesktop = data instanceof DesktopDragAndDropData; + + // Desktop DND + if (fromDesktop) { + const types = originalEvent.dataTransfer.types; + const typesArray: string[] = []; + for (let i = 0; i < types.length; i++) { + typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase + } + + if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) { + return false; + } + } + + // Other-Tree DND + else if (data instanceof ExternalElementsDragAndDropData) { + return false; + } + + // In-Explorer DND + else { + const sources: ExplorerItem[] = data.getData(); + if (!target) { + if (sources[0].isRoot) { + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: false }; + } + + return false; + } + + if (!Array.isArray(sources)) { + return false; + } + + if (sources.some((source) => { + if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) { + return true; // Root folder can not be moved to a non root file stat. + } + + if (source.resource.toString() === target.resource.toString()) { + return true; // Can not move anything onto itself + } + + if (source.isRoot && target instanceof ExplorerItem && target.isRoot) { + // Disable moving workspace roots in one another + return false; + } + + if (!isCopy && dirname(source.resource).toString() === target.resource.toString()) { + return true; // Can not move a file to the same parent unless we copy + } + + if (isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) { + return true; // Can not move a parent folder into one of its children + } + + return false; + })) { + return false; + } + } + + // All (target = model) + if (!target) { + return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? { accept: true, bubble: TreeDragOverBubble.Down } : false; // can only drop folders to workspace + } + + // All (target = file/folder) + else { + const effect = fromDesktop || isCopy ? ListDragOverEffect.Copy : ListDragOverEffect.Move; + + if (target.isDirectory) { + if (target.isReadonly) { + return false; + } + + return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: true }; + } + + if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) { + return { accept: true, bubble: TreeDragOverBubble.Up, effect }; + } + } + + return false; + } + + getDragURI(element: ExplorerItem): string { + return element.resource.toString(); + } + + getDragLabel(elements: ExplorerItem[]): string { + if (elements.length > 1) { + return String(elements.length); + } + + return elements[0].name; + } + + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + const sources: ExplorerItem[] = data.getData(); + if (sources && sources.length) { + // Apply some datatransfer types to allow for dragging the element outside of the application + this.instantiationService.invokeFunction(fillResourceDataTransfers, sources, originalEvent); + + // The only custom data transfer we set from the explorer is a file transfer + // to be able to DND between multiple code file explorers across windows + const fileResources = sources.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); + if (fileResources.length) { + originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources)); + } + } + } + + drop(data: IDragAndDropData, target: ExplorerItem, targetIndex: number, originalEvent: DragEvent): void { + // Desktop DND (Import file) + if (data instanceof DesktopDragAndDropData) { + this.handleExternalDrop(data, target, originalEvent); + } + + // In-Explorer DND (Move/Copy file) + else { + this.handleExplorerDrop(data, target, originalEvent); + } + } + + + private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + const droppedResources = extractResources(originalEvent, true); + + // Check for dropped external files to be folders + return this.fileService.resolveFiles(droppedResources).then(result => { + + // Pass focus to window + this.windowService.focusWindow(); + + // Handle folders by adding to workspace if we are in workspace context + const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource })); + if (folders.length > 0) { + + // If we are in no-workspace context, ask for confirmation to create a workspace + let confirmedPromise: Promise = Promise.resolve({ confirmed: true }); + if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) { + confirmedPromise = this.dialogService.confirm({ + message: folders.length > 1 ? localize('dropFolders', "Do you want to add the folders to the workspace?") : localize('dropFolder', "Do you want to add the folder to the workspace?"), + type: 'question', + primaryButton: folders.length > 1 ? localize('addFolders', "&&Add Folders") : localize('addFolder', "&&Add Folder") + }); + } + + return confirmedPromise.then(res => { + if (res.confirmed) { + return this.workspaceEditingService.addFolders(folders); + } + + return undefined; + }); + } + + // Handle dropped files (only support FileStat as target) + else if (target instanceof ExplorerItem && !target.isReadonly) { + return this.addResources(target, droppedResources.map(res => res.resource)); + } + + return undefined; + }); + } + + private addResources(target: ExplorerItem, resources: URI[]): Promise { + if (resources && resources.length > 0) { + + // Find parent to add to + if (!target) { + target = this.explorerService.roots[0]; + } + + if (!target.isDirectory) { + target = target.parent; + } + + // Resolve target to check for name collisions and ask user + return this.fileService.resolveFile(target.resource).then((targetStat: IFileStat) => { + + // Check for name collisions + const targetNames = new Set(); + targetStat.children.forEach((child) => { + targetNames.add(isLinux ? child.name : child.name.toLowerCase()); + }); + + let overwritePromise: Promise = Promise.resolve({ confirmed: true }); + if (resources.some(resource => { + return targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()); + })) { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), + detail: localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + overwritePromise = this.dialogService.confirm(confirm); + } + + return overwritePromise.then(res => { + if (!res.confirmed) { + return undefined; + } + + // Run add in sequence + const addPromisesFactory: ITask>[] = []; + resources.forEach(resource => { + addPromisesFactory.push(() => { + const sourceFile = resource; + const targetFile = joinPath(target.resource, basename(sourceFile)); + + // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents + // of the target file would replace the contents of the added file. since we already + // confirmed the overwrite before, this is OK. + let revertPromise: Promise = Promise.resolve(null); + if (this.textFileService.isDirty(targetFile)) { + revertPromise = this.textFileService.revertAll([targetFile], { soft: true }); + } + + return revertPromise.then(() => { + const copyTarget = joinPath(target.resource, basename(sourceFile)); + return this.fileService.copyFile(sourceFile, copyTarget, true).then(stat => { + + // if we only add one file, just open it directly + if (resources.length === 1) { + this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); + } + }); + }); + }); + }); + + return sequence(addPromisesFactory); + }); + }); + } + + return Promise.resolve(undefined); + } + + private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem | undefined, originalEvent: DragEvent): Promise { + const sources: ExplorerItem[] = distinctParents(data.getData(), s => s.resource); + const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); + + let confirmPromise: Promise; + + // Handle confirm setting + const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); + if (confirmDragAndDrop) { + confirmPromise = this.dialogService.confirm({ + message: sources.length > 1 && sources.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") + : sources.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", sources.length), sources.map(s => s.resource)) + : sources[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", sources[0].name) + : localize('confirmMove', "Are you sure you want to move '{0}'?", sources[0].name), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + }, + type: 'question', + primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") + }); + } else { + confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult); + } + + return confirmPromise.then(res => { + + // Check for confirmation checkbox + let updateConfirmSettingsPromise: Promise = Promise.resolve(undefined); + if (res.confirmed && res.checkboxChecked === true) { + updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER); + } + + return updateConfirmSettingsPromise.then(() => { + if (res.confirmed) { + const rootDropPromise = this.doHandleRootDrop(sources.filter(s => s.isRoot), target); + return Promise.all(sources.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined); + } + + return Promise.resolve(undefined); + }); + }); + } + + private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem | undefined): Promise { + if (roots.length === 0) { + return Promise.resolve(undefined); + } + + const folders = this.contextService.getWorkspace().folders; + let targetIndex: number; + const workspaceCreationData: IWorkspaceFolderCreationData[] = []; + const rootsToMove: IWorkspaceFolderCreationData[] = []; + + for (let index = 0; index < folders.length; index++) { + const data = { + uri: folders[index].uri + }; + if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) { + targetIndex = workspaceCreationData.length; + } + + if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) { + workspaceCreationData.push(data); + } else { + rootsToMove.push(data); + } + } + if (!target) { + // Empty area + targetIndex = workspaceCreationData.length; + } + + workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); + return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); + } + + private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem | undefined, isCopy: boolean): Promise { + if (!(target instanceof ExplorerItem)) { + return Promise.resolve(undefined); + } + + if (target.isReadonly) { + return undefined; + } + + // Reuse duplicate action if user copies + if (isCopy) { + + return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory })).then(stat => { + if (!stat.isDirectory) { + return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).then(() => undefined); + } + + return undefined; + }); + } + + // Otherwise move + const targetResource = joinPath(target.resource, source.name); + + return this.textFileService.move(source.resource, targetResource).then(undefined, error => { + + // Conflict + if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { + const confirm: IConfirmation = { + message: localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), + detail: localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + // Move with overwrite if the user confirms + return this.dialogService.confirm(confirm).then(res => { + if (res.confirmed) { + return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error)); + } + + return undefined; + }); + } + + // Any other error + else { + this.notificationService.error(error); + } + + return undefined; + }); + } +} From bfe8ab5b09a7e42a15d899267e2558eb0f98dce6 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 10 Jan 2019 15:59:58 +0100 Subject: [PATCH 50/65] explorer: drag and drop polish --- .../electron-browser/watchExpressionsView.ts | 6 ++-- .../electron-browser/views/explorerViewer.ts | 34 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index 581cfda36f7..ce2a21d53f5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -290,10 +290,10 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { } drop(data: IDragAndDropData, targetElement: IExpression): void { - const draggedData = data.getData(); + const expressions = (data as ElementsDragAndDropData).elements; - if (Array.isArray(draggedData)) { - const draggedElement = draggedData[0].element.element; + if (Array.isArray(expressions)) { + const draggedElement = expressions[0]; const watches = this.debugService.getModel().getWatchExpressions(); const position = targetElement instanceof Expression ? watches.indexOf(targetElement) : watches.length - 1; this.debugService.moveWatchExpression(draggedElement.getId(), position); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index fec0dea1640..4aaeadee93f 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -35,7 +35,7 @@ import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from ' import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; -import { DesktopDragAndDropData, ExternalElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; @@ -450,20 +450,21 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // In-Explorer DND else { - const sources: ExplorerItem[] = data.getData(); + const items = (data as ElementsDragAndDropData).elements; + if (!target) { - if (sources[0].isRoot) { + if (items[0].isRoot) { return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: false }; } return false; } - if (!Array.isArray(sources)) { + if (!Array.isArray(items)) { return false; } - if (sources.some((source) => { + if (items.some((source) => { if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) { return true; // Root folder can not be moved to a non root file stat. } @@ -529,14 +530,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { - const sources: ExplorerItem[] = data.getData(); - if (sources && sources.length) { + const items = (data as ElementsDragAndDropData).elements; + if (items && items.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, sources, originalEvent); + this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent); // The only custom data transfer we set from the explorer is a file transfer // to be able to DND between multiple code file explorers across windows - const fileResources = sources.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); + const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath); if (fileResources.length) { originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources)); } @@ -674,7 +675,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem | undefined, originalEvent: DragEvent): Promise { - const sources: ExplorerItem[] = distinctParents(data.getData(), s => s.resource); + const elementsData = (data as ElementsDragAndDropData).elements; + const items = distinctParents(elementsData, s => s.resource); const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); let confirmPromise: Promise; @@ -683,10 +685,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); if (confirmDragAndDrop) { confirmPromise = this.dialogService.confirm({ - message: sources.length > 1 && sources.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") - : sources.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", sources.length), sources.map(s => s.resource)) - : sources[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", sources[0].name) - : localize('confirmMove', "Are you sure you want to move '{0}'?", sources[0].name), + message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") + : items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", items.length), items.map(s => s.resource)) + : items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name) + : localize('confirmMove', "Are you sure you want to move '{0}'?", items[0].name), checkbox: { label: localize('doNotAskAgain', "Do not ask me again") }, @@ -707,8 +709,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return updateConfirmSettingsPromise.then(() => { if (res.confirmed) { - const rootDropPromise = this.doHandleRootDrop(sources.filter(s => s.isRoot), target); - return Promise.all(sources.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined); + const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target); + return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined); } return Promise.resolve(undefined); From 79818ff2f9441ea091d748f1d439efba89a017c0 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 10 Jan 2019 17:56:59 +0100 Subject: [PATCH 51/65] accept drop on root --- .../parts/files/electron-browser/views/explorerViewer.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 4aaeadee93f..300b24fe096 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -453,11 +453,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const items = (data as ElementsDragAndDropData).elements; if (!target) { - if (items[0].isRoot) { - return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: false }; - } - - return false; + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: false }; } if (!Array.isArray(items)) { From a8520a86dc451542e422e6cabc6d9bba1dddd1a0 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 11 Jan 2019 11:13:18 +0100 Subject: [PATCH 52/65] explorer dnd: properly compute drop target --- .../electron-browser/views/explorerViewer.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 300b24fe096..b06374ab048 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -541,11 +541,21 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } drop(data: IDragAndDropData, target: ExplorerItem, targetIndex: number, originalEvent: DragEvent): void { + // Find parent to add to + if (!target) { + target = this.explorerService.roots[this.explorerService.roots.length - 1]; + } + if (!target.isDirectory) { + target = target.parent; + } + if (target.isReadonly) { + return; + } + // Desktop DND (Import file) if (data instanceof DesktopDragAndDropData) { this.handleExternalDrop(data, target, originalEvent); } - // In-Explorer DND (Move/Copy file) else { this.handleExplorerDrop(data, target, originalEvent); @@ -586,7 +596,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } // Handle dropped files (only support FileStat as target) - else if (target instanceof ExplorerItem && !target.isReadonly) { + else if (target instanceof ExplorerItem) { return this.addResources(target, droppedResources.map(res => res.resource)); } @@ -597,15 +607,6 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private addResources(target: ExplorerItem, resources: URI[]): Promise { if (resources && resources.length > 0) { - // Find parent to add to - if (!target) { - target = this.explorerService.roots[0]; - } - - if (!target.isDirectory) { - target = target.parent; - } - // Resolve target to check for name collisions and ask user return this.fileService.resolveFile(target.resource).then((targetStat: IFileStat) => { @@ -670,7 +671,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return Promise.resolve(undefined); } - private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem | undefined, originalEvent: DragEvent): Promise { + private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const elementsData = (data as ElementsDragAndDropData).elements; const items = distinctParents(elementsData, s => s.resource); const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); @@ -714,7 +715,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); } - private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem | undefined): Promise { + private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise { if (roots.length === 0) { return Promise.resolve(undefined); } @@ -738,24 +739,12 @@ export class FileDragAndDrop implements ITreeDragAndDrop { rootsToMove.push(data); } } - if (!target) { - // Empty area - targetIndex = workspaceCreationData.length; - } workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); } - private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem | undefined, isCopy: boolean): Promise { - if (!(target instanceof ExplorerItem)) { - return Promise.resolve(undefined); - } - - if (target.isReadonly) { - return undefined; - } - + private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise { // Reuse duplicate action if user copies if (isCopy) { From 69e75ec17f74d10b9236d1534c403a46c2497bf3 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 11 Jan 2019 11:47:26 +0100 Subject: [PATCH 53/65] explorer: do not always recursvily refresh --- .../parts/files/electron-browser/views/explorerView.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index e32a1a11cd2..c538216e9fd 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -350,15 +350,17 @@ export class ExplorerView extends ViewletPanel { /** * Refresh the contents of the explorer to get up to date data from the disk about the file structure. + * If the item is passed we refresh only that level of the tree, otherwise we do a full refresh. */ refresh(item?: ExplorerItem): Promise { if (!this.tree || !this.isBodyVisible()) { this.shouldRefresh = true; return Promise.resolve(undefined); } + const recursive = !item; const toRefresh = item || this.tree.getInput(); - return this.tree.refresh(toRefresh, true); + return this.tree.refresh(toRefresh, recursive); } getOptimalWidth(): number { From b52a3280c11a25c57322be2a9226eded16c07b16 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 11 Jan 2019 15:47:57 +0100 Subject: [PATCH 54/65] file icons fixes #64894 --- src/vs/base/browser/ui/tree/abstractTree.ts | 2 +- src/vs/base/browser/ui/tree/media/tree.css | 1 + .../workbench/browser/parts/views/customView.ts | 2 ++ .../browser/parts/views/media/views.css | 13 ++++++++++++- src/vs/workbench/browser/parts/views/views.ts | 17 ++++++++++++++++- .../electron-browser/views/explorerView.ts | 3 +++ .../electron-browser/fileIconThemeData.ts | 6 +++++- 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 31046489dcd..ecd6078b7cd 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -193,7 +193,7 @@ class TreeRenderer implements IListRenderer { const findMatchHighlightColor = theme.getColor(editorFindMatchHighlight); if (findMatchHighlightColor) { collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); + collector.addRule(`.file-icon-themable-tree .monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); } const findMatchHighlightColorBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightColorBorder) { collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); + collector.addRule(`.file-icon-themable-tree .monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); } const link = theme.getColor(textLinkForeground); if (link) { diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index b4f83027944..ec2880220b8 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* File icon themeable tree style */ +/* File icon themeable OLD tree style */ .file-icon-themable-tree .monaco-tree-row .content { display: flex; } @@ -50,6 +50,17 @@ display: none; } +/* File icons in trees */ + +.file-icon-themable-tree.align-icons-and-twisties .monaco-tl-twistie:not(.collapsible), +.file-icon-themable-tree.hide-arrows .monaco-tl-twistie { + background-image: none !important; + width: 0 !important; + margin-right: 0 !important; +} + +/* Misc */ + .monaco-workbench .tree-explorer-viewlet-tree-view { height: 100%; } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 44ff6623fd2..b0e7153d2f1 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/views'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IViewsService, ViewsRegistry, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IView, IViewDescriptorCollection } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -19,6 +19,8 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { localize } from 'vs/nls'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { values } from 'vs/base/common/map'; +import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { toggleClass, addClass } from 'vs/base/browser/dom'; function filterViewEvent(container: ViewContainer, event: Event): Event { return Event.chain(event) @@ -612,3 +614,16 @@ export class ViewsService extends Disposable implements IViewsService { return contextKey; } } + +export function createFileIconThemableTreeContainerScope(container: HTMLElement, themeService: IWorkbenchThemeService): IDisposable { + addClass(container, 'file-icon-themable-tree'); + addClass(container, 'show-file-icons'); + + const onDidChangeFileIconTheme = (theme: IFileIconTheme) => { + toggleClass(container, 'align-icons-and-twisties', theme.hasFileIcons && !theme.hasFolderIcons); + toggleClass(container, 'hide-arrows', theme.hidesExplorerArrows === true); + }; + + onDidChangeFileIconTheme(themeService.getFileIconTheme()); + return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); +} \ No newline at end of file diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index c538216e9fd..625d1deacbf 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -42,6 +42,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/browser/parts/views/views'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; @@ -225,6 +226,8 @@ export class ExplorerView extends ViewletPanel { const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels); this.disposables.push(filesRenderer); + this.disposables.push(createFileIconThemableTreeContainerScope(container, this.themeService)); + this.tree = new WorkbenchAsyncDataTree(container, new ExplorerDelegate(), [filesRenderer], this.instantiationService.createInstance(ExplorerDataSource), { accessibilityProvider: new ExplorerAccessibilityProvider(), diff --git a/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts index d1f0a073413..57f7a7a8669 100644 --- a/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts @@ -196,7 +196,8 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i qualifier = baseThemeClassName + ' ' + qualifier; } - let expanded = '.monaco-tree-row.expanded'; // workaround for #11453 + const expanded = '.monaco-tree-row.expanded'; // workaround for #11453 + const expanded2 = '.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents'; // new tree if (associations.folder) { addSelector(`${qualifier} .folder-icon::before`, associations.folder); @@ -205,6 +206,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i if (associations.folderExpanded) { addSelector(`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded); + addSelector(`${qualifier} ${expanded2} .folder-icon::before`, associations.folderExpanded); result.hasFolderIcons = true; } @@ -218,6 +220,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i if (rootFolderExpanded) { addSelector(`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded); + addSelector(`${qualifier} ${expanded2} .rootfolder-icon::before`, rootFolderExpanded); result.hasFolderIcons = true; } @@ -237,6 +240,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i if (folderNamesExpanded) { for (let folderName in folderNamesExpanded) { addSelector(`${qualifier} ${expanded} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNamesExpanded[folderName]); + addSelector(`${qualifier} ${expanded2} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNamesExpanded[folderName]); result.hasFolderIcons = true; } } From bf245d196b97b025cbcae896d83d91b068fd3f1d Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 11 Jan 2019 16:33:19 +0100 Subject: [PATCH 55/65] move properly on file events --- src/vs/workbench/parts/files/electron-browser/explorerService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index e459bba9f83..d51ea23ddb8 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -192,6 +192,7 @@ export class ExplorerService implements IExplorerService { // Move in Model modelElements.forEach((modelElement, index) => { const oldParent = modelElement.parent; + modelElement.move(newParents[index]); this._onDidChangeItem.fire(oldParent); this._onDidChangeItem.fire(newParents[index]); }); From bb21186a5cc3006d7f92b0c5f46a8fe1d75ef78a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 11 Jan 2019 16:41:10 +0100 Subject: [PATCH 56/65] tree: clear auto expand disposable on drop --- src/vs/base/browser/ui/tree/abstractTree.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ecd6078b7cd..65c8b83ef84 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -104,6 +104,9 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< } drop(data: IDragAndDropData, targetNode: ITreeNode | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { + this.autoExpandDisposable.dispose(); + this.autoExpandNode = undefined; + this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent); } } From 3763ab7b0a47d76ae453a4d4c0076a38e727deff Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 11 Jan 2019 17:13:48 +0100 Subject: [PATCH 57/65] async data tree: clear cached refresh promises on setinput fixes #65500 --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 40 ++++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index a3ec69680f8..4bbcc410b6d 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -9,12 +9,13 @@ import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOve import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { timeout, always } from 'vs/base/common/async'; +import { timeout, always, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { toggleClass } from 'vs/base/browser/dom'; import { Iterator } from 'vs/base/common/iterator'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; enum AsyncDataTreeNodeState { Uninitialized, @@ -221,7 +222,7 @@ export class AsyncDataTree implements IDisposable private readonly tree: ObjectTree, TFilterData>; private readonly root: IAsyncDataTreeNode; private readonly nodes = new Map>(); - private readonly refreshPromises = new Map, Promise>(); + private readonly refreshPromises = new Map, CancelablePromise>(); private readonly identityProvider?: IIdentityProvider; private readonly _onDidChangeNodeState = new Emitter>(); @@ -327,6 +328,9 @@ export class AsyncDataTree implements IDisposable } setInput(input: TInput): Promise { + this.refreshPromises.forEach(promise => promise.cancel()); + this.refreshPromises.clear(); + this.root.element = input!; return this.refresh(input); } @@ -486,25 +490,13 @@ export class AsyncDataTree implements IDisposable } private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { - await this._refreshNode(node, recursive, reason); + await this.doRefresh(node, recursive, reason); if (recursive && node.children) { await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason))); } } - private _refreshNode(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { - let result = this.refreshPromises.get(node); - - if (result) { - return result; - } - - result = this.doRefresh(node, recursive, reason); - this.refreshPromises.set(node, result); - return always(result, () => this.refreshPromises.delete(node)); - } - private doRefresh(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { const hasChildren = !!this.dataSource.hasChildren(node.element!); @@ -524,7 +516,7 @@ export class AsyncDataTree implements IDisposable this._onDidChangeNodeState.fire(node); }, _ => null); - return Promise.resolve(this.dataSource.getChildren(node.element!)) + return Promise.resolve(this.doGetChildren(node)) .then(children => { slowTimeout.cancel(); node.state = AsyncDataTreeNodeState.Loaded; @@ -533,6 +525,10 @@ export class AsyncDataTree implements IDisposable this.setChildren(node, children, recursive); this._onDidResolveChildren.fire({ element: node.element as T, reason }); }, err => { + if (isPromiseCanceledError(err)) { + return Promise.resolve(null); + } + slowTimeout.cancel(); node.state = AsyncDataTreeNodeState.Uninitialized; this._onDidChangeNodeState.fire(node); @@ -546,6 +542,18 @@ export class AsyncDataTree implements IDisposable } } + private doGetChildren(node: IAsyncDataTreeNode): Promise { + let result = this.refreshPromises.get(node); + + if (result) { + return result; + } + + result = createCancelablePromise(_ => Promise.resolve(this.dataSource.getChildren(node.element!))); + this.refreshPromises.set(node, result); + return always(result, () => this.refreshPromises.delete(node)); + } + private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent, any>): void { if (!node.collapsed && node.element.state === AsyncDataTreeNodeState.Uninitialized) { if (deep) { From a71334aedd129e6dc2a924db7cc070d3b4ecd2cf Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 11 Jan 2019 17:58:19 +0100 Subject: [PATCH 58/65] explicit refresh should drop cash --- src/vs/workbench/parts/files/common/files.ts | 1 + .../parts/files/electron-browser/explorerService.ts | 5 +++++ .../parts/files/electron-browser/fileActions.ts | 12 +++++------- .../files/electron-browser/views/explorerView.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index f80f5166a8b..f7c25973e45 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -49,6 +49,7 @@ export interface IExplorerService { setEditable(stat: ExplorerItem, data: IEditableData): void; getEditableData(stat: ExplorerItem): IEditableData | undefined; findClosest(resource: URI): ExplorerItem | null; + refresh(): void; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index d51ea23ddb8..3e6c9da424c 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -134,6 +134,11 @@ export class ExplorerService implements IExplorerService { }); } + refresh(): void { + this.model.roots.forEach(r => r.isDirectoryResolved = false); + this._onDidChangeItem.fire(null); + } + // File events private onFileOperation(e: FileOperationEvent): void { diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index db6b2c76bcf..95c5873db29 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -853,18 +853,16 @@ export class RefreshExplorerView extends Action { constructor( id: string, label: string, - @IViewletService private readonly viewletService: IViewletService + @IViewletService private readonly viewletService: IViewletService, + @IExplorerService private readonly explorerService: IExplorerService ) { super(id, label, 'explorer-action refresh-explorer'); } public run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true).then((viewlet: ExplorerViewlet) => { - const explorerView = viewlet.getExplorerView(); - if (explorerView) { - explorerView.refresh(); - } - }); + return this.viewletService.openViewlet(VIEWLET_ID, true).then(() => + this.explorerService.refresh() + ); } } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 625d1deacbf..d56dc3b72db 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -355,7 +355,7 @@ export class ExplorerView extends ViewletPanel { * Refresh the contents of the explorer to get up to date data from the disk about the file structure. * If the item is passed we refresh only that level of the tree, otherwise we do a full refresh. */ - refresh(item?: ExplorerItem): Promise { + private refresh(item?: ExplorerItem): Promise { if (!this.tree || !this.isBodyVisible()) { this.shouldRefresh = true; return Promise.resolve(undefined); From 91e42bf266a61113920178673fb415fb051ed17a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 14 Jan 2019 10:06:31 +0100 Subject: [PATCH 59/65] remove children resolution event --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 28 +++++--------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 4bbcc410b6d..823a1e055cd 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -115,16 +115,6 @@ function asTreeContextMenuEvent(e: ITreeContextMenuEvent { - readonly element: T | null; - readonly reason: ChildrenResolutionReason; -} - function asAsyncDataTreeDragAndDropData(data: IDragAndDropData): IDragAndDropData { if (data instanceof ElementsDragAndDropData) { const nodes = (data as ElementsDragAndDropData>).elements; @@ -233,9 +223,6 @@ export class AsyncDataTree implements IDisposable get onDidChangeSelection(): Event> { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); } get onDidOpen(): Event> { return Event.map(this.tree.onDidOpen, asTreeEvent); } - private readonly _onDidResolveChildren = new Emitter>(); - readonly onDidResolveChildren: Event> = this._onDidResolveChildren.event; - get onMouseClick(): Event> { return Event.map(this.tree.onMouseClick, asTreeMouseEvent); } get onMouseDblClick(): Event> { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); } get onContextMenu(): Event> { return Event.map(this.tree.onContextMenu, asTreeContextMenuEvent); } @@ -340,7 +327,7 @@ export class AsyncDataTree implements IDisposable throw new Error('Tree input not set'); } - return this.refreshNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh); + return this.refreshNode(this.getDataNode(element), recursive); } // Tree @@ -365,7 +352,7 @@ export class AsyncDataTree implements IDisposable this.tree.expand(node, recursive); if (node.state !== AsyncDataTreeNodeState.Loaded) { - await this.refreshNode(node, false, ChildrenResolutionReason.Expand); + await this.refreshNode(node, false); } return true; @@ -489,15 +476,15 @@ export class AsyncDataTree implements IDisposable return node; } - private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { - await this.doRefresh(node, recursive, reason); + private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean): Promise { + await this.doRefresh(node, recursive); if (recursive && node.children) { - await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason))); + await Promise.all(node.children.map(child => this.refreshNode(child, recursive))); } } - private doRefresh(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { + private doRefresh(node: IAsyncDataTreeNode, recursive: boolean): Promise { const hasChildren = !!this.dataSource.hasChildren(node.element!); if (!hasChildren) { @@ -523,7 +510,6 @@ export class AsyncDataTree implements IDisposable this._onDidChangeNodeState.fire(node); this.setChildren(node, children, recursive); - this._onDidResolveChildren.fire({ element: node.element as T, reason }); }, err => { if (isPromiseCanceledError(err)) { return Promise.resolve(null); @@ -559,7 +545,7 @@ export class AsyncDataTree implements IDisposable if (deep) { this.collapse(node.element.element as T); } else { - this.refreshNode(node.element, false, ChildrenResolutionReason.Expand); + this.refreshNode(node.element, false); } } } From 2f18659fdc1f9d3503fcdafd4a1ee98ca7bd7062 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 14 Jan 2019 10:41:43 +0100 Subject: [PATCH 60/65] async data tree: prevent conflicting operations fixes #66407 --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 43 +++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 823a1e055cd..5cf1330feab 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -21,7 +21,8 @@ enum AsyncDataTreeNodeState { Uninitialized, Loaded, Loading, - Slow + Slow, + Disposed } interface IAsyncDataTreeNode { @@ -32,6 +33,16 @@ interface IAsyncDataTreeNode { state: AsyncDataTreeNodeState; } +function isAncestor(ancestor: IAsyncDataTreeNode, descendant: IAsyncDataTreeNode): boolean { + if (!descendant.parent) { + return false; + } else if (descendant.parent === ancestor) { + return true; + } else { + return isAncestor(ancestor, descendant.parent); + } +} + interface IDataTreeListTemplateData { templateData: T; } @@ -477,13 +488,39 @@ export class AsyncDataTree implements IDisposable } private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean): Promise { - await this.doRefresh(node, recursive); + await this.queueRefresh(node, recursive); if (recursive && node.children) { await Promise.all(node.children.map(child => this.refreshNode(child, recursive))); } } + private currentRefreshCalls = new Map, Promise>(); + + private async queueRefresh(node: IAsyncDataTreeNode, recursive: boolean): Promise { + if (node.state === AsyncDataTreeNodeState.Disposed) { + console.error('Async data tree node is disposed'); + return; + } + + let result: Promise | undefined; + + this.currentRefreshCalls.forEach((refreshPromise, refreshNode) => { + if (isAncestor(refreshNode, node) || isAncestor(node, refreshNode)) { + result = refreshPromise.then(() => this.queueRefresh(node, recursive)); + } + }); + + if (result) { + return result; + } + + result = this.doRefresh(node, recursive); + + this.currentRefreshCalls.set(node, result); + return always(result, () => this.currentRefreshCalls.delete(node)); + } + private doRefresh(node: IAsyncDataTreeNode, recursive: boolean): Promise { const hasChildren = !!this.dataSource.hasChildren(node.element!); @@ -491,6 +528,7 @@ export class AsyncDataTree implements IDisposable this.setChildren(node, [], recursive); return Promise.resolve(); } else if (node !== this.root && (!this.tree.isCollapsible(node) || this.tree.isCollapsed(node))) { + node.state = AsyncDataTreeNodeState.Uninitialized; return Promise.resolve(); } else { node.state = AsyncDataTreeNodeState.Loading; @@ -636,6 +674,7 @@ export class AsyncDataTree implements IDisposable const onDidDeleteNode = (treeNode: ITreeNode, TFilterData>) => { if (treeNode.element.element) { if (!insertedElements.has(treeNode.element.element as T)) { + treeNode.element.state = AsyncDataTreeNodeState.Disposed; this.nodes.delete(treeNode.element.element as T); } } From 7090af71a002f7566abf73debd4295e35b01051f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 14 Jan 2019 11:12:44 +0100 Subject: [PATCH 61/65] async data tree: always check for root fixes #66408 --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 13 +++++++------ .../files/electron-browser/views/explorerView.ts | 12 ++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 5cf1330feab..cf3972fc432 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -350,17 +350,18 @@ export class AsyncDataTree implements IDisposable } collapse(element: T, recursive: boolean = false): boolean { - return this.tree.collapse(this.getDataNode(element), recursive); + const node = this.getDataNode(element); + return this.tree.collapse(node === this.root ? null : node, recursive); } async expand(element: T, recursive: boolean = false): Promise { const node = this.getDataNode(element); - if (!this.tree.isCollapsed(node)) { + if (!this.tree.isCollapsed(node === this.root ? null : node)) { return false; } - this.tree.expand(node, recursive); + this.tree.expand(node === this.root ? null : node, recursive); if (node.state !== AsyncDataTreeNodeState.Loaded) { await this.refreshNode(node, false); @@ -558,7 +559,7 @@ export class AsyncDataTree implements IDisposable this._onDidChangeNodeState.fire(node); if (node !== this.root) { - this.tree.collapse(node); + this.tree.collapse(node === this.root ? null : node); } return Promise.reject(err); @@ -632,12 +633,12 @@ export class AsyncDataTree implements IDisposable asyncDataTreeNode.element = element; const collapsible = !!this.dataSource.hasChildren(element); - const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode); + const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode === this.root ? null : asyncDataTreeNode); if (recursive) { asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized; - if (this.tree.isCollapsed(asyncDataTreeNode)) { + if (this.tree.isCollapsed(asyncDataTreeNode === this.root ? null : asyncDataTreeNode)) { asyncDataTreeNode.children!.length = 0; return { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index d56dc3b72db..492c178177d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; -import { sequence, ignoreErrors } from 'vs/base/common/async'; +import { sequence } from 'vs/base/common/async'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; @@ -165,10 +165,14 @@ export class ExplorerView extends ViewletPanel { const isEditing = !!this.explorerService.getEditableData(e); if (isEditing) { this.tree.setFocus([]); - expandPromise = ignoreErrors(this.tree.expand(e.parent)); + expandPromise = this.tree.expand(e.parent); } DOM.toggleClass(this.tree.getHTMLElement(), 'highlight', isEditing); - expandPromise.then(() => this.refresh(e.parent)); + expandPromise.then(() => this.refresh(e.parent)).then(() => { + if (isEditing) { + this.tree.reveal(e); + } + }); })); this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal))); @@ -435,7 +439,7 @@ export class ExplorerView extends ViewletPanel { parent = parent.parent; } - return sequence(toExpand.reverse().map(s => () => ignoreErrors(this.tree.expand(s)))).then(() => { + return sequence(toExpand.reverse().map(s => () => this.tree.expand(s))).then(() => { if (reveal) { this.tree.reveal(fileStat, 0.5); } From bf7e222d4d9050ea8dc1aba169f12671a008f716 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 14 Jan 2019 11:15:12 +0100 Subject: [PATCH 62/65] list: clear drag feedback when drop is rejected fixes #66476 --- src/vs/base/browser/ui/list/listView.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 481fa107067..4ea4a78061c 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -661,6 +661,8 @@ export class ListView implements ISpliceable, IDisposable { const canDrop = typeof result === 'boolean' ? result : result.accept; if (!canDrop) { + this.currentDragFeedback = undefined; + this.currentDragFeedbackDisposable.dispose(); return false; } From 64e95a886fc77c1029d7a5ec8768326a35058768 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 14 Jan 2019 14:47:21 +0100 Subject: [PATCH 63/65] Global new file and new folder actions should be scoped by the explorer's focused item fixes #66475 --- .../files/electron-browser/fileActions.ts | 18 ++++++++++-------- .../electron-browser/views/explorerView.ts | 8 ++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 95c5873db29..918c4f35be5 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -103,7 +103,7 @@ export class NewFileAction extends BaseErrorReportingAction { static readonly LABEL = nls.localize('createNewFile', "New File"); constructor( - private element: ExplorerItem, + private getElement: () => ExplorerItem, @INotificationService notificationService: INotificationService, @IExplorerService private explorerService: IExplorerService, @IFileService private fileService: IFileService, @@ -115,8 +115,9 @@ export class NewFileAction extends BaseErrorReportingAction { run(): Promise { let folder: ExplorerItem; - if (this.element) { - folder = this.element.isDirectory ? this.element : this.element.parent; + const element = this.getElement(); + if (element) { + folder = element.isDirectory ? element : element.parent; } else { folder = this.explorerService.roots[0]; } @@ -157,7 +158,7 @@ export class NewFolderAction extends BaseErrorReportingAction { static readonly LABEL = nls.localize('createNewFolder', "New Folder"); constructor( - private element: ExplorerItem, + private getElement: () => ExplorerItem, @INotificationService notificationService: INotificationService, @IFileService private fileService: IFileService, @IExplorerService private explorerService: IExplorerService @@ -168,8 +169,9 @@ export class NewFolderAction extends BaseErrorReportingAction { run(): Promise { let folder: ExplorerItem; - if (this.element) { - folder = this.element.isDirectory ? this.element : this.element.parent; + const element = this.getElement(); + if (element) { + folder = element.isDirectory ? element : element.parent; } else { folder = this.explorerService.roots[0]; } @@ -1040,7 +1042,7 @@ function getContext(listWidget: ListWidget): IExplorerContext { // TODO@isidor these commands are calling into actions due to the complex inheritance action structure. // It should be the other way around, that actions call into commands. -function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature1): Promise { +function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature1<() => ExplorerItem, Action>): Promise { const instantationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); const viewletService = accessor.get(IViewletService); @@ -1055,7 +1057,7 @@ function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: ICons if (explorerView && explorerView.isBodyVisible()) { explorerView.focus(); const { stat } = getContext(listService.lastFocusedList); - const action = instantationService.createInstance(constructor, stat); + const action = instantationService.createInstance(constructor, () => stat); return action.run(); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 492c178177d..c710b6b01fb 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -209,8 +209,12 @@ export class ExplorerView extends ViewletPanel { getActions(): IAction[] { const actions: Action[] = []; - actions.push(this.instantiationService.createInstance(NewFileAction, undefined)); - actions.push(this.instantiationService.createInstance(NewFolderAction, undefined)); + const getFocus = () => { + const focus = this.tree.getFocus(); + return focus.length > 0 ? focus[0] : undefined; + }; + actions.push(this.instantiationService.createInstance(NewFileAction, getFocus)); + actions.push(this.instantiationService.createInstance(NewFolderAction, getFocus)); actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL)); actions.push(this.instantiationService.createInstance(CollapseAction2, this.tree, true, 'explorer-action collapse-explorer')); From 68fa8e78354bb94741ea7583c056bd8187fe6a9b Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 14 Jan 2019 14:54:33 +0100 Subject: [PATCH 64/65] fixes #66477 --- .../parts/files/electron-browser/views/explorerViewer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index b06374ab048..d377ac121c8 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -453,6 +453,11 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const items = (data as ElementsDragAndDropData).elements; if (!target) { + // Droping onto the empty area. Do not accept if items dragged are already children of the root + if (items.every(i => i.parent.isRoot)) { + return false; + } + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: false }; } From 8d7303fbddf880b0ce83557d54336548fcf6f9a0 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 14 Jan 2019 15:34:45 +0100 Subject: [PATCH 65/65] list: should not fwd drop when customer says no drop --- src/vs/base/browser/ui/list/listView.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 4ea4a78061c..6b77d619d40 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -173,6 +173,7 @@ export class ListView implements ISpliceable, IDisposable { private supportDynamicHeights: boolean; private dnd: IListViewDragAndDrop; + private canDrop: boolean = false; private currentDragData: IDragAndDropData | undefined; private currentDragFeedback: number[] | undefined; private currentDragFeedbackDisposable: IDisposable = Disposable.None; @@ -658,9 +659,9 @@ export class ListView implements ISpliceable, IDisposable { } const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.browserEvent); - const canDrop = typeof result === 'boolean' ? result : result.accept; + this.canDrop = typeof result === 'boolean' ? result : result.accept; - if (!canDrop) { + if (!this.canDrop) { this.currentDragFeedback = undefined; this.currentDragFeedbackDisposable.dispose(); return false; @@ -729,6 +730,10 @@ export class ListView implements ISpliceable, IDisposable { } private onDrop(event: IListDragEvent): void { + if (!this.canDrop) { + return; + } + const dragData = this.currentDragData; this.teardownDragAndDropScrollTopAnimation(); this.clearDragOverFeedback(); @@ -745,6 +750,7 @@ export class ListView implements ISpliceable, IDisposable { } private onDragEnd(): void { + this.canDrop = false; this.teardownDragAndDropScrollTopAnimation(); this.clearDragOverFeedback(); this.currentDragData = undefined;