diff --git a/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.css b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css similarity index 72% rename from src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.css rename to src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css index 7a30f6791f431f56ca33f543e9d7f10fab6c4042..f1556526d688e2211a2eb264431d7d6a69b6ed2d 100644 --- a/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.css +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css @@ -9,8 +9,6 @@ flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; - --item-hover-background: green; - --item-hover-color: inhert; } .monaco-breadcrumbs .monaco-breadcrumb-item { @@ -19,15 +17,7 @@ flex: 0 1 auto; white-space: nowrap; cursor: pointer; -} - -.monaco-breadcrumbs:focus .monaco-breadcrumb-item.active { - color: pink; -} - -.monaco-breadcrumbs .monaco-breadcrumb-item:hover { - color: var(--item-hover-color); - background-color: var(--item-hover-background); + align-self: center; } .monaco-breadcrumbs .monaco-breadcrumb-item-more { diff --git a/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts similarity index 94% rename from src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.ts rename to src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index f11495d93a6c87164e13924555af7e55d9b28344..e83364656e65c384382c0df98ef971fbb6db6ad6 100644 --- a/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -38,11 +38,12 @@ export class SimpleBreadcrumbsItem extends BreadcrumbsItem { export class RenderedBreadcrumbsItem extends BreadcrumbsItem { - + readonly element: E; private _disposables: IDisposable[] = []; constructor(render: (element: E, container: HTMLDivElement, bucket: IDisposable[]) => any, element: E, more: boolean) { super(document.createElement('div'), more); + this.element = element; render(element, this.node as HTMLDivElement, this._disposables); } @@ -57,7 +58,6 @@ export class BreadcrumbsWidget { private readonly _disposables = new Array(); private readonly _domNode: HTMLDivElement; private readonly _scrollable: DomScrollableElement; - private _cachedWidth: number; private readonly _onDidSelectItem = new Emitter(); private readonly _onDidChangeFocus = new Emitter(); @@ -100,10 +100,12 @@ export class BreadcrumbsWidget { this._freeNodes.length = 0; } - layout(width: number = this._cachedWidth): void { - if (typeof width === 'number') { - this._cachedWidth = width; - this._domNode.style.width = `${this._cachedWidth}px`; + layout(dim: dom.Dimension): void { + if (!dim) { + this._scrollable.scanDomNode(); + } else { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; this._scrollable.scanDomNode(); } } @@ -124,7 +126,7 @@ export class BreadcrumbsWidget { private _focus(nth: number): boolean { if (this._focusedItemIdx !== -1) { - dom.removeClass(this._nodes[this._focusedItemIdx], 'active'); + dom.removeClass(this._nodes[this._focusedItemIdx], 'focused'); this._focusedItemIdx = -1; } if (nth < 0 || nth >= this._nodes.length) { @@ -132,7 +134,7 @@ export class BreadcrumbsWidget { } this._focusedItemIdx = nth; let node = this._nodes[this._focusedItemIdx]; - dom.addClass(node, 'active'); + dom.addClass(node, 'focused'); this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft }); return true; } @@ -177,7 +179,7 @@ export class BreadcrumbsWidget { this._domNode.appendChild(node); this._nodes[start] = node; } - this.layout(); + this.layout(undefined); this._focus(this._nodes.length - 1); } diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 74518dd76c18023123c9e9a8f383f4966b801f73..f9b0c858a38737e8255f909964e255853a2d076b 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -20,6 +20,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const EDITOR_TITLE_HEIGHT = 35; +export const BREAD_CRUMPS_HEIGHT = 30; export const EDITOR_MIN_DIMENSIONS = new Dimension(220, 70); export const EDITOR_MAX_DIMENSIONS = new Dimension(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); @@ -159,4 +160,4 @@ export interface EditorGroupsServiceImpl extends IEditorGroupsService { * A promise that resolves when groups have been restored. */ readonly whenRestored: TPromise; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts b/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts new file mode 100644 index 0000000000000000000000000000000000000000..72f5ee5c6179e1cbd4542763d624bee823a53acb --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts @@ -0,0 +1,352 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/editorbreadcrumbs'; +import * as dom from 'vs/base/browser/dom'; +import { BreadcrumbsWidget, RenderedBreadcrumbsItem } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import * as paths from 'vs/base/common/paths'; +import URI from 'vs/base/common/uri'; +import { IContextKey, IContextKeyService, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { IEditorBreadcrumbs, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ITreeConfiguration, IDataSource, ITree, IRenderer, ISelectionEvent } from 'vs/base/parts/tree/browser/tree'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { isEqual } from 'vs/base/common/resources'; + +interface FileElement { + name: string; + uri: URI; + kind: FileKind; +} + +export class EditorBreadcrumbs implements IEditorBreadcrumbs { + + static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); + static CK_BreadcrumbsFocused = new RawContextKey('breadcrumbsFocused', false); + + private readonly _disposables = new Array(); + private readonly _domNode: HTMLDivElement; + private readonly _widget: BreadcrumbsWidget; + + private readonly _ckBreadcrumbsVisible: IContextKey; + private readonly _ckBreadcrumbsFocused: IContextKey; + + constructor( + container: HTMLElement, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IFileService private readonly _fileService: IFileService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + this._domNode = document.createElement('div'); + this._domNode.className = 'editor-breadcrumbs'; + this._widget = new BreadcrumbsWidget(this._domNode); + this._widget.onDidSelectItem(this._onDidSelectItem, this, this._disposables); + this._widget.onDidChangeFocus(val => this._ckBreadcrumbsFocused.set(val), undefined, this._disposables); + container.appendChild(this._domNode); + + this._ckBreadcrumbsVisible = EditorBreadcrumbs.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsFocused = EditorBreadcrumbs.CK_BreadcrumbsFocused.bindTo(this._contextKeyService); + } + + dispose(): void { + dispose(this._disposables); + this._widget.dispose(); + this._ckBreadcrumbsVisible.reset(); + } + + layout(dim: dom.Dimension): void { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._widget.layout(dim); + } + + setActive(value: boolean): void { + dom.toggleClass(this._domNode, 'active', value); + } + + openEditor(input: EditorInput): void { + + let uri = input.getResource(); + if (!uri || !this._fileService.canHandleResource(uri)) { + return this.closeEditor(undefined); + } + + this._ckBreadcrumbsVisible.set(true); + dom.toggleClass(this._domNode, 'hidden', false); + + + const render = (element: FileElement, target: HTMLElement, disposables: IDisposable[]) => { + let label = this._instantiationService.createInstance(FileLabel, target, {}); + label.setFile(element.uri, { fileKind: element.kind, hidePath: true }); + disposables.push(label); + }; + + let items: RenderedBreadcrumbsItem[] = []; + let workspace = this._workspaceService.getWorkspaceFolder(uri); + let path = uri.path; + + while (true) { + if (workspace && isEqual(workspace.uri, uri, true) || path === '/') { + break; + } + + let first = items.length === 0; + let name = paths.basename(path); + uri = uri.with({ path }); + path = paths.dirname(path); + items.unshift(new RenderedBreadcrumbsItem( + render, + { name, uri, kind: first ? FileKind.FILE : FileKind.FOLDER }, + !first + )); + } + + this._widget.replace(undefined, items); + } + + closeEditor(input: EditorInput): void { + this._ckBreadcrumbsVisible.set(false); + dom.toggleClass(this._domNode, 'hidden', true); + } + + focus(): void { + this._widget.focus(); + } + + focusNext(): void { + this._widget.focusNext(); + } + + focusPrev(): void { + this._widget.focusPrev(); + } + + select(): void { + this._widget.select(); + } + + private _onDidSelectItem(item: RenderedBreadcrumbsItem): void { + this._contextViewService.showContextView({ + getAnchor() { + return item.node; + }, + render: (container: HTMLElement) => { + let res = this._instantiationService.createInstance(BreadcrumbsFilePicker, container); + res.layout({ width: 250, height: 300 }); + res.setInput(item.element.uri.with({ path: paths.dirname(item.element.uri.path) })); + res.onDidPickElement(data => { + this._contextViewService.hideContextView(); + if (!data) { + return; + } + if (URI.isUri(data)) { + this._editorService.openEditor({ resource: data }); + } + }); + return res; + }, + }); + } +} + +export abstract class BreadcrumbsPicker { + + readonly focus: dom.IFocusTracker; + + protected readonly _onDidPickElement = new Emitter(); + readonly onDidPickElement: Event = this._onDidPickElement.event; + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _tree: WorkbenchTree; + + constructor( + container: HTMLElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IThemeService protected readonly _themeService: IThemeService, + ) { + this._domNode = document.createElement('div'); + // this._domNode.style.background = this._themeService.getTheme().getColor(colors.progressBarBackground).toString(); + container.appendChild(this._domNode); + + this._tree = this._instantiationService.createInstance(WorkbenchTree, this._domNode, this._completeTreeConfiguration({ dataSource: undefined }), {}); + debounceEvent(this._tree.onDidChangeSelection, (last, cur) => cur, 0)(this._onDidChangeSelection, this, this._disposables); + + this.focus = dom.trackFocus(this._domNode); + this.focus.onDidBlur(_ => this._onDidPickElement.fire(undefined), undefined, this._disposables); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this.focus.dispose(); + } + + layout(dim: dom.Dimension) { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._tree.layout(dim.height, dim.width); + } + + protected abstract _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration; + protected abstract _onDidChangeSelection(e: any): void; +} + +export class FileDataSource implements IDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + getId(tree: ITree, element: IFileStat | URI): string { + return URI.isUri(element) ? element.toString() : element.resource.toString(); + } + + hasChildren(tree: ITree, element: IFileStat | URI): boolean { + return URI.isUri(element) || element.isDirectory; + } + + getChildren(tree: ITree, element: IFileStat | URI): TPromise { + return this._fileService.resolveFile( + URI.isUri(element) ? element : element.resource + ).then(stat => { + for (const child of stat.children) { + this._parents.set(child, stat); + } + return stat.children; + }); + } + + getParent(tree: ITree, element: IFileStat | URI): TPromise { + return TPromise.as(URI.isUri(element) ? undefined : this._parents.get(element)); + } +} + +export class FileRenderer implements IRenderer { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return 'FileStat'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + return this._instantiationService.createInstance(FileLabel, container, {}); + } + + renderElement(tree: ITree, element: IFileStat, templateId: string, templateData: FileLabel): void { + templateData.setFile(element.resource, { hidePath: true, fileKind: element.isDirectory ? FileKind.FOLDER : FileKind.FILE }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } +} + +export class BreadcrumbsFilePicker extends BreadcrumbsPicker { + + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + config.dataSource = this._instantiationService.createInstance(FileDataSource); + config.renderer = this._instantiationService.createInstance(FileRenderer); + return config; + } + + setInput(resource: URI): void { + this._tree.domFocus(); + this._tree.setInput(resource); + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + let [first] = e.selection; + let stat = first as IFileStat; + if (stat && !stat.isDirectory) { + this._onDidPickElement.fire(stat.resource); + } + } +} + + +//#region commands + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focus', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: EditorBreadcrumbs.CK_BreadcrumbsVisible, + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focus(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusNext', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.RightArrow, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focusNext(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusPrevious', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.LeftArrow, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focusPrev(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectFocused', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Enter, + secondary: [KeyCode.UpArrow, KeyCode.Space], + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.select(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectEditor', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Escape, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.activeControl.focus(); + } +}); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0e9f49bd1512c9491d601fcb9a094cdf6144bf0c..97615f52698dffed91831469c22bf8a8d6a9c6dd 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -19,7 +19,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Themable, EDITOR_GROUP_HEADER_TABS_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder, IEditorBreadcrumbs } from 'vs/workbench/services/group/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -34,7 +34,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, EDITOR_TITLE_HEIGHT, EDITOR_MIN_DIMENSIONS, EDITOR_MAX_DIMENSIONS, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, EDITOR_TITLE_HEIGHT, EDITOR_MIN_DIMENSIONS, EDITOR_MAX_DIMENSIONS, getActiveTextEditorOptions, IEditorOpeningEvent, BREAD_CRUMPS_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { join } from 'vs/base/common/paths'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -46,6 +46,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { EditorBreadcrumbs } from 'vs/workbench/browser/parts/editor/editorBreadcrumbs'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -104,6 +105,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private titleContainer: HTMLElement; private titleAreaControl: TitleControl; + private breadcrumbsContainer: HTMLElement; + private breadcrumbsControl: EditorBreadcrumbs; + private progressBar: ProgressBar; private editorContainer: HTMLElement; @@ -190,6 +194,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control this.createTitleAreaControl(); + // Breadcrumbs container + this.breadcrumbsContainer = document.createElement('div'); + addClass(this.breadcrumbsContainer, 'editor-breadcrumbs'); + this.element.appendChild(this.breadcrumbsContainer); + + // Breadcrumbs control + this.createEditorBreadcrumbs(); + // Editor container this.editorContainer = document.createElement('div'); addClass(this.editorContainer, 'editor-container'); @@ -396,6 +408,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } + private createEditorBreadcrumbs(): void { + + if (this.breadcrumbsControl) { + this.breadcrumbsControl.dispose(); + clearNode(this.breadcrumbsContainer); + } + + this.breadcrumbsControl = this.scopedInstantiationService.createInstance(EditorBreadcrumbs, this.breadcrumbsContainer); + } + private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): TPromise { if (this._group.count === 0) { return TPromise.as(void 0); // nothing to show @@ -592,6 +614,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._label; } + get breadcrumbs(): IEditorBreadcrumbs { + return this.breadcrumbsControl; + } + get disposed(): boolean { return this._disposed; } @@ -617,6 +643,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update title control this.titleAreaControl.setActive(isActive); + this.breadcrumbsControl.setActive(isActive); + // Update styles this.updateStyles(); @@ -788,6 +816,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Show in title control after editor control because some actions depend on it this.titleAreaControl.openEditor(editor); + this.breadcrumbsControl.openEditor(editor); + return openEditorPromise; } @@ -955,8 +985,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseInactiveEditor(editor); } - // Forward to title control + // Forward to title control & breadcrumbs this.titleAreaControl.closeEditor(editor); + this.breadcrumbsControl.closeEditor(editor); } private doCloseActiveEditor(focusNext = this.accessor.activeGroup === this, fromError?: boolean): void { @@ -1342,7 +1373,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to controls this.titleAreaControl.layout(new Dimension(this.dimension.width, EDITOR_TITLE_HEIGHT)); - this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - EDITOR_TITLE_HEIGHT)); + this.breadcrumbsControl.layout(new Dimension(this.dimension.width, BREAD_CRUMPS_HEIGHT)); + this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - (EDITOR_TITLE_HEIGHT + BREAD_CRUMPS_HEIGHT))); } toJSON(): ISerializedEditorGroup { @@ -1362,6 +1394,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleAreaControl.dispose(); + this.breadcrumbsControl.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css b/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css new file mode 100644 index 0000000000000000000000000000000000000000..36815a8d225918959de204bc9b4e3b5307b9550a --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench>.part.editor>.content .editor-breadcrumbs .monaco-breadcrumbs { + --color-item-focused: pink; +} + +.monaco-workbench>.part.editor>.content .editor-breadcrumbs .monaco-breadcrumbs:focus .monaco-breadcrumb-item.focused { + color: var(--color-item-focused); +} diff --git a/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsPicker.ts b/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsPicker.ts new file mode 100644 index 0000000000000000000000000000000000000000..4deeeb916584efdd79b6651a38d1dcea5c0d89bb --- /dev/null +++ b/src/vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsPicker.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Dimension, trackFocus, IFocusTracker } from 'vs/base/browser/dom'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ITreeConfiguration, IDataSource, ITree, IRenderer, ISelectionEvent } from 'vs/base/parts/tree/browser/tree'; +import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IFileService, IFileStat, FileKind } from 'vs/platform/files/common/files'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import URI from 'vs/base/common/uri'; +import { Emitter, Event, debounceEvent } from 'vs/base/common/event'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export abstract class BreadcrumbsPicker { + + readonly focus: IFocusTracker; + + protected readonly _onDidPickElement = new Emitter(); + readonly onDidPickElement: Event = this._onDidPickElement.event; + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _tree: WorkbenchTree; + + constructor( + container: HTMLElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IThemeService protected readonly _themeService: IThemeService, + ) { + this._domNode = document.createElement('div'); + // this._domNode.style.background = this._themeService.getTheme().getColor(colors.progressBarBackground).toString(); + container.appendChild(this._domNode); + + this._tree = this._instantiationService.createInstance(WorkbenchTree, this._domNode, this._completeTreeConfiguration({ dataSource: undefined }), {}); + debounceEvent(this._tree.onDidChangeSelection, (last, cur) => cur, 0)(this._onDidChangeSelection, this, this._disposables); + + this.focus = trackFocus(this._domNode); + this.focus.onDidBlur(_ => this._onDidPickElement.fire(undefined), undefined, this._disposables); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this.focus.dispose(); + } + + layout(dim: Dimension) { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._tree.layout(dim.height, dim.width); + } + + protected abstract _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration; + protected abstract _onDidChangeSelection(e: any): void; +} + +export class FileDataSource implements IDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + getId(tree: ITree, element: IFileStat | URI): string { + return URI.isUri(element) ? element.toString() : element.resource.toString(); + } + + hasChildren(tree: ITree, element: IFileStat | URI): boolean { + return URI.isUri(element) || element.isDirectory; + } + + getChildren(tree: ITree, element: IFileStat | URI): TPromise { + return this._fileService.resolveFile( + URI.isUri(element) ? element : element.resource + ).then(stat => { + for (const child of stat.children) { + this._parents.set(child, stat); + } + return stat.children; + }); + } + + getParent(tree: ITree, element: IFileStat | URI): TPromise { + return TPromise.as(URI.isUri(element) ? undefined : this._parents.get(element)); + } +} + +export class FileRenderer implements IRenderer { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return 'FileStat'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + return this._instantiationService.createInstance(FileLabel, container, {}); + } + + renderElement(tree: ITree, element: IFileStat, templateId: string, templateData: FileLabel): void { + templateData.setFile(element.resource, { hidePath: true, fileKind: element.isDirectory ? FileKind.FOLDER : FileKind.FILE }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } +} + +export class BreadcrumbsFilePicker extends BreadcrumbsPicker { + + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + config.dataSource = this._instantiationService.createInstance(FileDataSource); + config.renderer = this._instantiationService.createInstance(FileRenderer); + return config; + } + + setInput(resource: URI): void { + this._tree.domFocus(); + this._tree.setInput(resource); + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + let [first] = e.selection; + let stat = first as IFileStat; + if (stat && !stat.isDirectory) { + this._onDidPickElement.fire(stat.resource); + } + } +} diff --git a/src/vs/workbench/parts/breadcrumbs/electron-browser/editorBreadcrumbs.ts b/src/vs/workbench/parts/breadcrumbs/electron-browser/editorBreadcrumbs.ts index 970656cab15b26fc25a7862a3b39e5dfaf628f6e..a45d920e4a96e3ee0715f0d01901704f22e27327 100644 --- a/src/vs/workbench/parts/breadcrumbs/electron-browser/editorBreadcrumbs.ts +++ b/src/vs/workbench/parts/breadcrumbs/electron-browser/editorBreadcrumbs.ts @@ -7,7 +7,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { BreadcrumbsWidget, RenderedBreadcrumbsItem } from 'vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsWidget'; +import { BreadcrumbsWidget, RenderedBreadcrumbsItem } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; @@ -16,6 +16,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FileKind } from 'vs/platform/files/common/files'; import { FileLabel } from 'vs/workbench/browser/labels'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { BreadcrumbsFilePicker } from 'vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbsPicker'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; class Widget implements IOverlayWidget { @@ -59,6 +62,11 @@ class BreadcrumbsUpdateEvent { } } +interface FileElement { + name: string; + uri: URI; + kind: FileKind; +} export class EditorBreadcrumbs implements IEditorContribution { @@ -78,6 +86,8 @@ export class EditorBreadcrumbs implements IEditorContribution { readonly editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, ) { this._widget = new Widget(() => this._onWidgetReady()); this._update = new BreadcrumbsUpdateEvent(this.editor); @@ -99,6 +109,7 @@ export class EditorBreadcrumbs implements IEditorContribution { this._disposables.push(this._widget.breadcrumb.onDidChangeFocus(value => this._ckFocused.set(value))); this._disposables.push(this._widget.breadcrumb.onDidSelectItem(this._onDidSelectItem, this)); this._update.event(this._onUpdate, this, this._disposables); + this._onUpdate(); } private _onUpdate(): void { @@ -107,26 +118,20 @@ export class EditorBreadcrumbs implements IEditorContribution { } let { uri } = this.editor.getModel(); - interface Element { - name: string; - uri: URI; - kind: FileKind; - } - - const render = (element: Element, target: HTMLElement, disposables: IDisposable[]) => { + const render = (element: FileElement, target: HTMLElement, disposables: IDisposable[]) => { let label = this._instantiationService.createInstance(FileLabel, target, {}); label.setFile(element.uri, { fileKind: element.kind, hidePath: true }); disposables.push(label); }; - let items: RenderedBreadcrumbsItem[] = []; + let items: RenderedBreadcrumbsItem[] = []; let path = uri.path; while (path !== '/') { let first = items.length === 0; let name = posix.basename(path); uri = uri.with({ path }); path = posix.dirname(path); - items.unshift(new RenderedBreadcrumbsItem( + items.unshift(new RenderedBreadcrumbsItem( render, { name, uri, kind: first ? FileKind.FILE : FileKind.FOLDER }, !first @@ -136,8 +141,28 @@ export class EditorBreadcrumbs implements IEditorContribution { this._widget.breadcrumb.replace(undefined, items); } - private _onDidSelectItem(item: RenderedBreadcrumbsItem): void { - console.log(item); + private _onDidSelectItem(item: RenderedBreadcrumbsItem): void { + + this._contextViewService.showContextView({ + getAnchor() { + return item.node; + }, + render: (container: HTMLElement) => { + let res = this._instantiationService.createInstance(BreadcrumbsFilePicker, container); + res.layout({ width: 300, height: 450 }); + res.setInput(item.element.uri.with({ path: posix.dirname(item.element.uri.path) })); + res.onDidPickElement(data => { + this._contextViewService.hideContextView(); + if (!data) { + return; + } + if (URI.isUri(data)) { + this._editorService.openEditor({ resource: data }); + } + }); + return res; + }, + }); } focus(): void { diff --git a/src/vs/workbench/services/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index 138849ffa8e3ad0226516e4a2be6f0ea78e8904c..7df05d47a0ac2e0f128effb9df3caa8f4b2cc11b 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -312,6 +312,13 @@ export interface IGroupChangeEvent { editorIndex?: number; } +export interface IEditorBreadcrumbs { + focus(): void; + focusNext(): void; + focusPrev(): void; + select(): void; +} + export interface IEditorGroup { /** @@ -332,6 +339,11 @@ export interface IEditorGroup { */ readonly label: string; + /** + * + */ + readonly breadcrumbs: IEditorBreadcrumbs; + /** * The active control is the currently visible control of the group. */ @@ -476,4 +488,4 @@ export interface IEditorGroup { * Invoke a function in the context of the services of this group. */ invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T; -} \ No newline at end of file +} diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 34cd6c0f235dc2b89d9067ba269a58906cc44c59..4bb1eda080e88ae5b54ba992dc4d41550854089c 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -94,8 +94,6 @@ import 'vs/workbench/electron-browser/workbench'; import 'vs/workbench/parts/relauncher/electron-browser/relauncher.contribution'; -import 'vs/workbench/parts/breadcrumbs/electron-browser/breadcrumbs.contribution'; - import 'vs/workbench/parts/tasks/electron-browser/task.contribution'; import 'vs/workbench/parts/emmet/browser/emmet.browser.contribution';