From d87534808d6623cb06a0e4f9fd0407fed6fe29b1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 12 Sep 2017 17:29:29 +0200 Subject: [PATCH 01/67] list: dont cancel list mouse events --- src/vs/base/browser/ui/list/listWidget.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 3a4e10f7b26..9eb101a4f35 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -348,8 +348,6 @@ class MouseController implements IDisposable { } private onMouseDown(e: IListMouseEvent): void { - e.preventDefault(); - e.stopPropagation(); this.view.domNode.focus(); let reference = this.list.getFocus()[0]; @@ -373,9 +371,6 @@ class MouseController implements IDisposable { } private onPointer(e: IListMouseEvent): void { - e.preventDefault(); - e.stopPropagation(); - if (isSelectionChangeEvent(e)) { return; } @@ -388,9 +383,6 @@ class MouseController implements IDisposable { } private onDoubleClick(e: IListMouseEvent): void { - e.preventDefault(); - e.stopPropagation(); - if (isSelectionChangeEvent(e)) { return; } From 0e2ccd44a6e680c6a9b80c6c90ce90abf03d54d4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 12 Sep 2017 17:40:52 +0200 Subject: [PATCH 02/67] wip: better scm view management --- src/vs/workbench/browser/parts/views/views.ts | 9 +- .../scm/electron-browser/media/scmViewlet.css | 4 + .../parts/scm/electron-browser/scmViewlet.ts | 166 ++++++++++++++++-- 3 files changed, 160 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index d432db70921..3fe40af888f 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -414,9 +414,14 @@ export class ViewsViewlet extends Viewlet { } } - private toggleViewVisibility(id: string): void { + toggleViewVisibility(id: string, visible?: boolean): void { const view = this.getView(id); let viewState = this.viewsStates.get(id); + + if ((visible === true && view) || (visible === false && !view)) { + return; + } + if (view) { viewState = viewState || this.createViewState(view); viewState.isHidden = true; @@ -557,7 +562,7 @@ export class ViewsViewlet extends Viewlet { private canBeVisible(viewDescriptor: IViewDescriptor): boolean { const viewstate = this.viewsStates.get(viewDescriptor.id); - if (viewDescriptor.canToggleVisibility && viewstate && viewstate.isHidden) { + if (viewstate && viewstate.isHidden) { return false; } return this.contextKeyService.contextMatchesRules(viewDescriptor.when); diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index c2027e6fd4f..ea7dce28868 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -27,6 +27,10 @@ height: 100%; } +.scm-viewlet .scm-provider { + display: flex; +} + .scm-viewlet .monaco-list-row { padding: 0 12px 0 20px; line-height: 22px; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 6fb5efcb992..0be3c5de734 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { chain } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Builder } from 'vs/base/browser/builder'; import { PersistentViewsViewlet, CollapsibleView, IViewletViewOptions, IViewletView, IViewOptions } from 'vs/workbench/browser/parts/views/views'; import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; @@ -78,9 +78,127 @@ function identityProvider(r: ISCMResourceGroup | ISCMResource): string { } } -interface SearchInputEvent extends Event { - target: HTMLInputElement; - immediate?: boolean; +interface IViewModel { + isRepositoryVisible(repository: ISCMRepository): boolean; + toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); +} + +class ProvidersViewDescriptor implements IViewDescriptor { + readonly id = 'providers'; + readonly name = ''; + readonly location = ViewLocation.SCM; + readonly ctor = null; +} + +class ProvidersListDelegate implements IDelegate { + + getHeight(element: ISCMRepository): number { + return 22; + } + + getTemplateId(element: ISCMRepository): string { + return 'provider'; + } +} + +interface RepositoryTemplateData { + checkbox: HTMLInputElement; + name: HTMLElement; + disposable: IDisposable; +} + +class ProviderRenderer implements IRenderer { + + readonly templateId = 'provider'; + + constructor( + protected viewModel: IViewModel + ) { } + + renderTemplate(container: HTMLElement): RepositoryTemplateData { + const provider = append(container, $('.scm-provider')); + const checkbox = append(provider, $('input', { type: 'checkbox', checked: 'true' })) as HTMLInputElement; + const name = append(provider, $('.name')); + + return { checkbox, name, disposable: EmptyDisposable }; + } + + renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + + templateData.name.textContent = repository.provider.label; + + templateData.checkbox.checked = this.viewModel.isRepositoryVisible(repository); + const onClick = domEvent(templateData.checkbox, 'change'); + templateData.disposable = onClick(() => this.viewModel.toggleRepositoryVisibility(repository, templateData.checkbox.checked)); + } + + disposeTemplate(templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + } +} + +class ProvidersView extends CollapsibleView { + + private list: List; + + constructor( + initialSize: number, + protected viewModel: IViewModel, + options: IViewletViewOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @ISCMService protected scmService: ISCMService + ) { + super(initialSize, { + ...(options as IViewOptions), + sizing: ViewSizing.Fixed, + name: localize('scm providers', "Source Control Providers"), + }, keybindingService, contextMenuService); + } + + renderHeader(container: HTMLElement): void { + const title = append(container, $('div.title')); + title.textContent = this.name; + + super.renderHeader(container); + } + + protected renderBody(container: HTMLElement): void { + const delegate = new ProvidersListDelegate(); + const renderer = new ProviderRenderer(this.viewModel); + this.list = new List(container, delegate, [renderer]); + + this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.toDispose); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.toDispose); + this.updateList(); + } + + layoutBody(size: number): void { + if (!this.list) { + return; + } + + this.list.layout(size); + } + + private updateList(): void { + this.list.splice(0, this.list.length, this.scmService.repositories); + } + + private onDidAddRepository(repository: ISCMRepository): void { + this.updateList(); + this.setBodySize(this.getExpandedBodySize()); + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + this.updateList(); + this.setBodySize(this.getExpandedBodySize()); + } + + private getExpandedBodySize(): number { + return Math.min(10, this.scmService.repositories.length) * 22; + } } interface ResourceGroupTemplate { @@ -219,7 +337,7 @@ class ResourceRenderer implements IRenderer { } } -class Delegate implements IDelegate { +class ProviderListDelegate implements IDelegate { getHeight() { return 22; } @@ -228,7 +346,7 @@ class Delegate implements IDelegate { } } -class SourceControlViewDescriptor implements IViewDescriptor { +class ProviderViewDescriptor implements IViewDescriptor { // This ID magic needs to happen in order to preserve // good splitview state when reloading the workbench @@ -243,19 +361,19 @@ class SourceControlViewDescriptor implements IViewDescriptor { get location(): ViewLocation { return ViewLocation.SCM; } constructor(private _repository: ISCMRepository) { - if (SourceControlViewDescriptor.freeIds.length > 0) { - this.id = SourceControlViewDescriptor.freeIds.shift(); + if (ProviderViewDescriptor.freeIds.length > 0) { + this.id = ProviderViewDescriptor.freeIds.shift(); } else { - this.id = `scm${SourceControlViewDescriptor.idCount++}`; + this.id = `scm${ProviderViewDescriptor.idCount++}`; } } dispose(): void { - SourceControlViewDescriptor.freeIds.push(this.id); + ProviderViewDescriptor.freeIds.push(this.id); } } -class SourceControlView extends CollapsibleView { +class ProviderView extends CollapsibleView { private cachedHeight: number | undefined; private inputBoxContainer: HTMLElement; @@ -327,7 +445,7 @@ class SourceControlView extends CollapsibleView { // List this.listContainer = append(container, $('.scm-status.show-file-icons')); - const delegate = new Delegate(); + const delegate = new ProviderListDelegate(); const actionItemProvider = (action: IAction) => this.getActionItem(action); @@ -494,7 +612,7 @@ class InstallAdditionalSCMProvidersAction extends Action { export class SCMViewlet extends PersistentViewsViewlet { private menus: SCMMenus; - private repositoryToViewDescriptor = new Map(); + private repositoryToViewDescriptor = new Map(); private disposables: IDisposable[] = []; constructor( @@ -515,7 +633,7 @@ export class SCMViewlet extends PersistentViewsViewlet { @IStorageService storageService: IStorageService, @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, ViewLocation.SCM, 'scm', true, + super(VIEWLET_ID, ViewLocation.SCM, 'scm', false, telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); this.menus = instantiationService.createInstance(SCMMenus, undefined); @@ -523,7 +641,7 @@ export class SCMViewlet extends PersistentViewsViewlet { } private onDidAddRepository(repository: ISCMRepository): void { - const viewDescriptor = new SourceControlViewDescriptor(repository); + const viewDescriptor = new ProviderViewDescriptor(repository); this.repositoryToViewDescriptor.set(repository.provider.id, viewDescriptor); ViewsRegistry.registerViews([viewDescriptor]); @@ -550,11 +668,25 @@ export class SCMViewlet extends PersistentViewsViewlet { this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); this.scmService.repositories.forEach(p => this.onDidAddRepository(p)); + + ViewsRegistry.registerViews([new ProvidersViewDescriptor()]); + } + + isRepositoryVisible(repository: ISCMRepository): boolean { + const view = this.repositoryToViewDescriptor.get(repository.provider.id); + return !!this.getView(view.id); + } + + toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean): void { + const view = this.repositoryToViewDescriptor.get(repository.provider.id); + this.toggleViewVisibility(view.id, visible); } protected createView(viewDescriptor: IViewDescriptor, initialSize: number, options: IViewletViewOptions): IViewletView { - if (viewDescriptor instanceof SourceControlViewDescriptor) { - return this.instantiationService.createInstance(SourceControlView, initialSize, viewDescriptor.repository, options); + if (viewDescriptor instanceof ProviderViewDescriptor) { + return this.instantiationService.createInstance(ProviderView, initialSize, viewDescriptor.repository, options); + } else if (viewDescriptor instanceof ProvidersViewDescriptor) { + return this.instantiationService.createInstance(ProvidersView, initialSize, this, options); } return this.instantiationService.createInstance(viewDescriptor.ctor, initialSize, options); From 1b314f3ef434adbd83e31478a81f99dbd0c4b16d Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 13 Sep 2017 12:09:54 +0200 Subject: [PATCH 03/67] scm: rootUri api --- extensions/git/src/repository.ts | 4 +-- src/vs/vscode.d.ts | 8 ++++- .../api/electron-browser/mainThreadSCM.ts | 14 +++----- src/vs/workbench/api/node/extHost.api.impl.ts | 4 +-- src/vs/workbench/api/node/extHost.protocol.ts | 2 +- src/vs/workbench/api/node/extHostSCM.ts | 11 ++++-- .../scm/electron-browser/media/scmViewlet.css | 9 ++++- .../parts/scm/electron-browser/scmViewlet.ts | 34 +++++++++++++++---- src/vs/workbench/services/scm/common/scm.ts | 1 + 9 files changed, 61 insertions(+), 26 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e2331a308e5..7f2e3e213c6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -382,9 +382,7 @@ export class Repository implements Disposable { const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path)); onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables); - const label = `${path.basename(repository.root)} (Git)`; - - this._sourceControl = scm.createSourceControl('git', label); + this._sourceControl = scm.createSourceControl('git', 'Git', Uri.parse(repository.root)); this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] }; this._sourceControl.quickDiffProvider = this; this.disposables.push(this._sourceControl); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index edfa2a8dc69..5f4723974a6 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5469,6 +5469,11 @@ declare module 'vscode' { */ readonly label: string; + /** + * The (optional) Uri of the root of this source control. + */ + readonly rootUri: Uri | undefined; + /** * The [input box](#SourceControlInputBox) for this source control. */ @@ -5537,9 +5542,10 @@ declare module 'vscode' { * * @param id An `id` for the source control. Something short, eg: `git`. * @param label A human-readable string for the source control. Eg: `Git`. + * @param rootUri An optional Uri of the root of the source control. Eg: `Uri.parse(workspaceRoot)`. * @return An instance of [source control](#SourceControl). */ - export function createSourceControl(id: string, label: string): SourceControl; + export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index cbfeac52f1d..64253cfa2d4 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -100,18 +100,17 @@ class MainThreadSCMProvider implements ISCMProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } + get rootUri(): URI | undefined { return this._rootUri; } get contextValue(): string { return this._contextValue; } get commitTemplate(): string | undefined { return this.features.commitTemplate; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } + get count(): number | undefined { return this.features.count; } private _onDidChangeCommitTemplate = new Emitter(); get onDidChangeCommitTemplate(): Event { return this._onDidChangeCommitTemplate.event; } - private _count: number | undefined = undefined; - get count(): number | undefined { return this._count; } - private _onDidChange = new Emitter(); get onDidChange(): Event { return this._onDidChange.event; } @@ -120,15 +119,12 @@ class MainThreadSCMProvider implements ISCMProvider { private _handle: number, private _contextValue: string, private _label: string, + private _rootUri: URI | undefined, @ISCMService scmService: ISCMService, @ICommandService private commandService: ICommandService ) { } $updateSourceControl(features: SCMProviderFeatures): void { - if ('count' in features) { - this._count = features.count; - } - this.features = assign(this.features, features); this._onDidChange.fire(); @@ -275,8 +271,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables = dispose(this._disposables); } - $registerSourceControl(handle: number, id: string, label: string): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, this.scmService, this.commandService); + $registerSourceControl(handle: number, id: string, label: string, rootUri: string | undefined): void { + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.parse(rootUri), this.scmService, this.commandService); const repository = this.scmService.registerSCMProvider(provider); this._repositories[handle] = repository; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index b1829b9387f..efe5e932d5c 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -486,14 +486,14 @@ export function createApiFactory( get inputBox() { return extHostSCM.getLastInputBox(extension); }, - createSourceControl(id: string, label: string) { + createSourceControl(id: string, label: string, rootUri?: vscode.Uri) { mainThreadTelemetry.$publicLog('registerSCMProvider', { extensionId: extension.id, providerId: id, providerLabel: label }); - return extHostSCM.createSourceControl(extension, id, label); + return extHostSCM.createSourceControl(extension, id, label, rootUri); } }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 0f5f2955d88..7245c7d80bd 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -353,7 +353,7 @@ export type SCMRawResourceSplices = [ ]; export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: string | undefined): void; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 31e5ace5eb1..ec9ad8b38ee 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -287,6 +287,10 @@ class ExtHostSourceControl implements vscode.SourceControl { return this._label; } + get rootUri(): vscode.Uri | undefined { + return this._rootUri; + } + private _inputBox: ExtHostSCMInputBox; get inputBox(): ExtHostSCMInputBox { return this._inputBox; } @@ -356,9 +360,10 @@ class ExtHostSourceControl implements vscode.SourceControl { private _commands: ExtHostCommands, private _id: string, private _label: string, + private _rootUri?: vscode.Uri ) { this._inputBox = new ExtHostSCMInputBox(this._proxy, this.handle); - this._proxy.$registerSourceControl(this.handle, _id, _label); + this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri && _rootUri.toString()); } private updatedResourceGroups = new Set(); @@ -468,9 +473,9 @@ export class ExtHostSCM { }); } - createSourceControl(extension: IExtensionDescription, id: string, label: string): vscode.SourceControl { + createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined): vscode.SourceControl { const handle = ExtHostSCM._handlePool++; - const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label); + const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label, rootUri); this._sourceControls.set(handle, sourceControl); const sourceControls = this._sourceControlsByExtension.get(extension.id) || []; diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index ea7dce28868..1f0fbc1ec5b 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -27,8 +27,15 @@ height: 100%; } -.scm-viewlet .scm-provider { +.scm-viewlet .monaco-list-row > .scm-provider { display: flex; + align-items: center; +} + +.scm-viewlet .scm-provider > .type { + opacity: 0.7; + margin-left: 0.5em; + font-size: 0.9em; } .scm-viewlet .monaco-list-row { diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 0be3c5de734..c6658aad5dc 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -9,6 +9,7 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { chain } from 'vs/base/common/event'; +import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Builder } from 'vs/base/browser/builder'; @@ -104,6 +105,7 @@ class ProvidersListDelegate implements IDelegate { interface RepositoryTemplateData { checkbox: HTMLInputElement; name: HTMLElement; + type: HTMLElement; disposable: IDisposable; } @@ -119,14 +121,21 @@ class ProviderRenderer implements IRenderer; + readonly rootUri?: URI; readonly count?: number; readonly commitTemplate?: string; readonly onDidChangeCommitTemplate?: Event; From c33b9942943d40a7ae3e1c14d1b5dd065fda51dc Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 13 Sep 2017 15:58:18 +0200 Subject: [PATCH 04/67] wip: checkboxes iteration --- .../ui/highlightedlabel/highlightedLabel.ts | 9 +- .../ui/octiconLabel/octiconLabel.mock.ts | 11 +-- .../browser/ui/octiconLabel/octiconLabel.ts | 13 +-- .../scm/electron-browser/media/scmViewlet.css | 11 ++- .../parts/scm/electron-browser/scmActivity.ts | 7 +- .../parts/scm/electron-browser/scmViewlet.ts | 98 ++++++++++++++++--- 6 files changed, 113 insertions(+), 36 deletions(-) diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 9f622f16fdb..95cf18196c1 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { escape } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import * as objects from 'vs/base/common/objects'; -import { expand as expandOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; +import { render as renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; export interface IHighlight { start: number; @@ -64,19 +63,19 @@ export class HighlightedLabel implements IDisposable { } if (pos < highlight.start) { htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(pos, highlight.start)))); + htmlContent.push(renderOcticons(this.text.substring(pos, highlight.start))); htmlContent.push(''); pos = highlight.end; } htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(highlight.start, highlight.end)))); + htmlContent.push(renderOcticons(this.text.substring(highlight.start, highlight.end))); htmlContent.push(''); pos = highlight.end; } if (pos < this.text.length) { htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(pos)))); + htmlContent.push(renderOcticons(this.text.substring(pos))); htmlContent.push(''); } diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts index 1f628552999..46bf06ccb39 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts @@ -5,8 +5,8 @@ import octiconLabel = require('vs/base/browser/ui/octiconLabel/octiconLabel'); import { escape } from 'vs/base/common/strings'; -function expand(text: string): string { - return text; +function render(text: string): string { + return escape(text); } class MockOcticonLabel { @@ -18,16 +18,13 @@ class MockOcticonLabel { } set text(text: string) { - let innerHTML = text || ''; - innerHTML = escape(innerHTML); - innerHTML = expand(innerHTML); - this._container.innerHTML = innerHTML; + this._container.innerHTML = render(text || ''); } } var mock: typeof octiconLabel = { - expand: expand, + render: render, OcticonLabel: MockOcticonLabel }; export = mock; \ No newline at end of file diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts index 50f4f0dbc76..e7339c58bbd 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts @@ -8,12 +8,16 @@ import 'vs/css!./octicons/octicons'; import 'vs/css!./octicons/octicons-animations'; import { escape } from 'vs/base/common/strings'; -export function expand(text: string): string { +function expand(text: string): string { return text.replace(/\$\(((.+?)(~(.*?))?)\)/g, (match, g1, name, g3, animation) => { return ``; }); } +export function render(label: string): string { + return expand(escape(label)); +} + export class OcticonLabel { private _container: HTMLElement; @@ -23,13 +27,10 @@ export class OcticonLabel { } set text(text: string) { - let innerHTML = text || ''; - innerHTML = escape(innerHTML); - innerHTML = expand(innerHTML); - this._container.innerHTML = innerHTML; + this._container.innerHTML = render(text || ''); } set title(title: string) { this._container.title = title; } -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index 1f0fbc1ec5b..137f20cd4b7 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -30,9 +30,18 @@ .scm-viewlet .monaco-list-row > .scm-provider { display: flex; align-items: center; + flex-wrap: wrap; } -.scm-viewlet .scm-provider > .type { +.scm-viewlet .monaco-list-row > .scm-provider > input { + flex-shrink: 0; +} + +.scm-viewlet .scm-provider > .name { + flex: 1; +} + +.scm-viewlet .scm-provider > .name > .type { opacity: 0.7; margin-left: 0.5em; font-size: 0.9em; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts index 81489a979b4..f5900b648ef 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts @@ -6,6 +6,7 @@ 'use strict'; import { localize } from 'vs/nls'; +import { basename } from 'vs/base/common/paths'; import { IDisposable, dispose, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { filterEvent, any as anyEvent } from 'vs/base/common/event'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; @@ -139,9 +140,13 @@ export class StatusBarController implements IWorkbenchContribution { this.statusBarDisposable.dispose(); const commands = repository.provider.statusBarCommands || []; + const label = repository.provider.rootUri + ? `${basename(repository.provider.rootUri.fsPath)} (${repository.provider.label})` + : repository.provider.label; + const disposables = commands.map(c => this.statusbarService.addEntry({ text: c.title, - tooltip: `${repository.provider.label} - ${c.tooltip}`, + tooltip: `${label} - ${c.tooltip}`, command: c.id, arguments: c.arguments }, MainThreadStatusBarAlignment.LEFT, 10000)); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index c6658aad5dc..3835e5e3cf5 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -11,7 +11,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { chain } from 'vs/base/common/event'; import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Builder } from 'vs/base/browser/builder'; import { PersistentViewsViewlet, CollapsibleView, IViewletViewOptions, IViewletView, IViewOptions } from 'vs/workbench/browser/parts/views/views'; import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; @@ -35,7 +35,7 @@ import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IAction, Action, IActionItem, ActionRunner } from 'vs/base/common/actions'; import { MenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem'; import { SCMMenus } from './scmMenus'; -import { ActionBar, IActionItemProvider, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, IActionItemProvider, Separator, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { isSCMResource } from './scmUtil'; import { attachListStyler, attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -52,6 +52,8 @@ import * as platform from 'vs/base/common/platform'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { Command } from 'vs/editor/common/modes'; +import { render as renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; // TODO@Joao // Need to subclass MenuItemActionItem in order to respect @@ -104,9 +106,39 @@ class ProvidersListDelegate implements IDelegate { interface RepositoryTemplateData { checkbox: HTMLInputElement; - name: HTMLElement; + title: HTMLElement; type: HTMLElement; + actionBar: ActionBar; disposable: IDisposable; + templateDisposable: IDisposable; +} + +class StatusBarAction extends Action { + + constructor( + private command: Command, + private commandService: ICommandService + ) { + super(`statusbaraction{${command.id}}`, command.title, '', true); + this.tooltip = command.tooltip; + } + + run(): TPromise { + return this.commandService.executeCommand(this.command.id, ...this.command.arguments); + } +} + +class StatusBarActionItem extends ActionItem { + + constructor(action: StatusBarAction) { + super(null, action, {}); + } + + _updateLabel(): void { + if (this.options.label) { + this.$e.innerHtml(renderOcticons(this.getAction().label)); + } + } } class ProviderRenderer implements IRenderer { @@ -114,36 +146,68 @@ class ProviderRenderer implements IRenderer new StatusBarActionItem(a as StatusBarAction) }); + const disposable = EmptyDisposable; + const templateDisposable = combinedDisposable([actionBar]); - return { checkbox, name, type, disposable: EmptyDisposable }; + return { checkbox, title, type, actionBar, disposable, templateDisposable }; } renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { templateData.disposable.dispose(); + const disposables: IDisposable[] = []; if (repository.provider.rootUri) { - templateData.name.textContent = basename(repository.provider.rootUri.fsPath); + templateData.title.textContent = basename(repository.provider.rootUri.fsPath); templateData.type.textContent = repository.provider.label; } else { - templateData.name.textContent = repository.provider.label; + templateData.title.textContent = repository.provider.label; templateData.type.textContent = ''; } templateData.checkbox.checked = this.viewModel.isRepositoryVisible(repository); const onClick = domEvent(templateData.checkbox, 'change'); - templateData.disposable = onClick(() => this.viewModel.toggleRepositoryVisibility(repository, templateData.checkbox.checked)); + disposables.push(onClick(() => this.viewModel.toggleRepositoryVisibility(repository, templateData.checkbox.checked))); + + // const disposables = commands.map(c => this.statusbarService.addEntry({ + // text: c.title, + // tooltip: `${repository.provider.label} - ${c.tooltip}`, + // command: c.id, + // arguments: c.arguments + // }, MainThreadStatusBarAlignment.LEFT, 10000)); + + const actions = []; + const disposeActions = () => dispose(actions); + disposables.push({ dispose: disposeActions }); + + const updateActions = () => { + disposeActions(); + + const commands = repository.provider.statusBarCommands || []; + actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService))); + templateData.actionBar.clear(); + templateData.actionBar.push(actions); + }; + + repository.provider.onDidChange(updateActions, null, disposables); + updateActions(); + + templateData.disposable = combinedDisposable(disposables); } disposeTemplate(templateData: RepositoryTemplateData): void { templateData.disposable.dispose(); + templateData.templateDisposable.dispose(); } } @@ -157,7 +221,8 @@ class ProvidersView extends CollapsibleView { options: IViewletViewOptions, @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, - @ISCMService protected scmService: ISCMService + @ISCMService protected scmService: ISCMService, + @IInstantiationService private instantiationService: IInstantiationService ) { super(initialSize, { ...(options as IViewOptions), @@ -175,7 +240,7 @@ class ProvidersView extends CollapsibleView { protected renderBody(container: HTMLElement): void { const delegate = new ProvidersListDelegate(); - const renderer = new ProviderRenderer(this.viewModel); + const renderer = this.instantiationService.createInstance(ProviderRenderer, this.viewModel); this.list = new List(container, delegate, [renderer]); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.toDispose); @@ -418,15 +483,16 @@ class ProviderView extends CollapsibleView { } renderHeader(container: HTMLElement): void { - const title = append(container, $('.title.scm-provider')); - const name = append(title, $('span.name')); - const type = append(title, $('span.type')); + const header = append(container, $('.title.scm-provider')); + const name = append(header, $('.name')); + const title = append(name, $('span.title')); + const type = append(name, $('span.type')); if (this.repository.provider.rootUri) { - name.textContent = basename(this.repository.provider.rootUri.fsPath); + title.textContent = basename(this.repository.provider.rootUri.fsPath); type.textContent = this.repository.provider.label; } else { - name.textContent = this.repository.provider.label; + title.textContent = this.repository.provider.label; type.textContent = ''; } From 1d5aa40c48287e76261d387d9a1bd031e43770e6 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 10:22:20 +0200 Subject: [PATCH 05/67] fix inputbox npe --- src/vs/base/browser/ui/inputbox/inputBox.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 18b09e697e8..603024b7c73 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -179,7 +179,13 @@ export class InputBox extends Widget { }); } - setTimeout(() => this.updateMirror(), 0); + setTimeout(() => { + if (!this.input) { + return; + } + + this.updateMirror(); + }, 0); // Support actions if (this.options.actions) { From acfad63ec5a8e6db4383c866098277a0ceb67ed1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 10:22:33 +0200 Subject: [PATCH 06/67] event relay --- src/vs/base/common/event.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index cd322eb4b29..9ca573aac02 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import CallbackList from 'vs/base/common/callbackList'; import { EventEmitter } from 'vs/base/common/eventEmitter'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -528,3 +528,21 @@ export function echo(event: Event, nextTick = false, buffer: T[] = []): Ev return emitter.event; } + +export class Relay implements IDisposable { + + private emitter = new Emitter(); + readonly output: Event = this.emitter.event; + + private disposable: IDisposable = EmptyDisposable; + + set input(event: Event) { + this.disposable.dispose(); + this.disposable = event(this.emitter.fire, this.emitter); + } + + dispose() { + this.disposable.dispose(); + this.emitter.dispose(); + } +} \ No newline at end of file From 09050b6aef914a52d382b72c163c4031fe2b45d9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 15:43:46 +0200 Subject: [PATCH 07/67] splitview: initial tests --- .../base/browser/ui/splitview/splitview2.ts | 325 ++++++++++++++++++ .../browser/ui/splitview/splitview.test.ts | 118 +++++++ 2 files changed, 443 insertions(+) create mode 100644 src/vs/base/browser/ui/splitview/splitview2.ts create mode 100644 src/vs/base/test/browser/ui/splitview/splitview.test.ts diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts new file mode 100644 index 00000000000..a65ad774a40 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -0,0 +1,325 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./splitview'; +import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; +import types = require('vs/base/common/types'); +import dom = require('vs/base/browser/dom'); +import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; +export { Orientation } from 'vs/base/browser/ui/sash/sash'; + +interface ISashEvent { + sash: Sash; + start: number; + current: number; +} + +export interface IOptions { + orientation?: Orientation; // default Orientation.VERTICAL + canChangeOrderByDragAndDrop?: boolean; +} + +export interface IView { + readonly minimumSize: number; + readonly maximumSize: number; + readonly onDidChange: Event; + render(container: HTMLElement, orientation: Orientation): void; + layout(size: number, orientation: Orientation): void; + focus(): void; +} + +class ViewItem { + + public explicitSize: number; + + constructor( + readonly view: IView, + readonly container: HTMLElement, + public size: number, + private orientation: Orientation, + private disposables: IDisposable[] + ) { + + } + + layout(): void { + if (this.orientation === Orientation.VERTICAL) { + this.container.style.height = `${this.size}px`; + } else { + this.container.style.width = `${this.size}px`; + } + + this.view.layout(this.size, this.orientation); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +interface ISashItem { + sash: Sash; + disposable: IDisposable; +} + +export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { + + private orientation: Orientation; + + private el: HTMLElement; + // private size: number; + // private viewElements: HTMLElement[]; + private viewItems: ViewItem[] = []; + private sashItems: ISashItem[] = []; + // private viewChangeListeners: IDisposable[]; + // private viewFocusPreviousListeners: IDisposable[]; + // private viewFocusNextListeners: IDisposable[]; + // private viewFocusListeners: IDisposable[]; + // private viewDnDListeners: IDisposable[][]; + // private sashOrientation: Orientation; + // private sashes: Sash[]; + // private sashesListeners: IDisposable[]; + // private eventWrapper: (event: ISashEvent) => ISashEvent; + // private animationTimeout: number; + // private state: IState; + + // private _onFocus: Emitter = this._register(new Emitter()); + // readonly onFocus: Event = this._onFocus.event; + + // private _onDidOrderChange: Emitter = this._register(new Emitter()); + // readonly onDidOrderChange: Event = this._onDidOrderChange.event; + + get length(): number { + return this.viewItems.length; + } + + constructor(private container: HTMLElement, options?: IOptions) { + options = options || {}; + this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; + + this.el = document.createElement('div'); + dom.addClass(this.el, 'monaco-split-view'); + dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal'); + container.appendChild(this.el); + + // this.size = null; + // this.viewElements = []; + // this.views = []; + // this.viewChangeListeners = []; + // this.viewFocusPreviousListeners = []; + // this.viewFocusNextListeners = []; + // this.viewFocusListeners = []; + // this.viewDnDListeners = []; + // this.sashes = []; + // this.sashesListeners = []; + // this.animationTimeout = null; + + // this.sashOrientation = this.orientation === Orientation.VERTICAL + // ? Orientation.HORIZONTAL + // : Orientation.VERTICAL; + + // if (this.orientation === Orientation.VERTICAL) { + // this.eventWrapper = e => { return { start: e.startY, current: e.currentY }; }; + // } else { + // this.eventWrapper = e => { return { start: e.startX, current: e.currentX }; }; + // } + + // The void space exists to handle the case where all other views are fixed size + // this.addView(new VoidView(), 0); + } + + private getContainerSize(): number { + return this.orientation === Orientation.VERTICAL + ? dom.getContentHeight(this.container) + : dom.getContentWidth(this.container); + } + + addView(view: IView, size: number, index = this.viewItems.length - 1): void { + // Create view container + const container = document.createElement('div'); + dom.addClass(container, 'split-view-view'); + const containerDisposable = toDisposable(() => this.el.removeChild(container)); + + // List to change events + const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); + + // Create item + const item = new ViewItem(view, container, size, this.orientation, [onChangeDisposable, containerDisposable]); + this.viewItems.splice(index, 0, item); + + // Render view + view.render(container, this.orientation); + + // Attach view + if (this.viewItems.length === 1) { + this.el.appendChild(container); + } else { + this.el.insertBefore(container, this.el.children.item(index)); + } + + // Add sash + if (this.viewItems.length <= 1) { + return; + } + + const orientation = this.orientation === Orientation.VERTICAL + ? Orientation.HORIZONTAL + : Orientation.VERTICAL; + + const sash = new Sash(this.el, this, { orientation }); + const sashEventMapper = this.orientation === Orientation.VERTICAL + ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) + : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); + + const onStart = mapEvent(fromEventEmitter(sash, 'start'), sashEventMapper); + const onStartDisposable = onStart(this.onSashStart, this); + + const onChange = mapEvent(fromEventEmitter(sash, 'change'), sashEventMapper); + const onSashChangeDisposable = onChange(this.onSashChange, this); + + const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); + + const sashItem: ISashItem = { + sash, + disposable + }; + + this.sashItems.splice(index - 1, 0, sashItem); + } + + removeView(index: number): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + // Remove view + const viewItem = this.viewItems.splice(index, 1)[0]; + viewItem.dispose(); + + if (this.viewItems.length < 1) { + return; + } + + // Remove sash + const sashIndex = Math.max(index - 1, 0); + const sashItem = this.sashItems.splice(sashIndex, 1)[0]; + sashItem.disposable.dispose(); + } + + layout(size?: number): void { + size = size || this.getContainerSize(); + } + + private onSashStart({ sash, start, current }: ISashEvent): void { + + } + + private onSashChange({ sash, start, current }: ISashEvent): void { + + } + + // Main algorithm + // private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void { + // let totalCollapse = collapse; + // let totalExpand = totalCollapse; + + // collapseIndexes.forEach(i => { + // let collapse = Math.min(collapses[i], totalCollapse); + // totalCollapse -= collapse; + // this.views[i].size -= collapse; + // }); + + // expandIndexes.forEach(i => { + // let expand = Math.min(expands[i], totalExpand); + // totalExpand -= expand; + // this.views[i].size += expand; + // }); + // } + + private getLastFlexibleViewIndex(exceptIndex: number = null): number { + // for (let i = this.views.length - 1; i >= 0; i--) { + // if (exceptIndex === i) { + // continue; + // } + // if (this.views[i].sizing === ViewSizing.Flexible) { + // return i; + // } + // } + + return -1; + } + + private layoutViews(): void { + this.viewItems.forEach(item => item.layout()); + this.sashItems.forEach(item => item.sash.layout()); + + // Update sashes enablement + // let previous = false; + // let collapsesDown = this.views.map(v => previous = (v.size - v.minimumSize > 0) || previous); + + // previous = false; + // let expandsDown = this.views.map(v => previous = (v.maximumSize - v.size > 0) || previous); + + // let reverseViews = this.views.slice().reverse(); + // previous = false; + // let collapsesUp = reverseViews.map(v => previous = (v.size - v.minimumSize > 0) || previous).reverse(); + + // previous = false; + // let expandsUp = reverseViews.map(v => previous = (v.maximumSize - v.size > 0) || previous).reverse(); + + // this.sashes.forEach((s, i) => { + // if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) { + // s.enable(); + // } else { + // s.disable(); + // } + // }); + } + + private onViewChange(view: ViewItem): void { + } + + // private setupAnimation(): void { + // if (types.isNumber(this.animationTimeout)) { + // window.clearTimeout(this.animationTimeout); + // } + + // dom.addClass(this.el, 'animated'); + // this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200); + // } + + // private clearAnimation(): void { + // this.animationTimeout = null; + // dom.removeClass(this.el, 'animated'); + // } + + getVerticalSashLeft(sash: Sash): number { + return this.getSashPosition(sash); + } + + getHorizontalSashTop(sash: Sash): number { + return this.getSashPosition(sash); + } + + private getSashPosition(sash: Sash): number { + let position = 0; + + for (let i = 0; i < this.sashItems.length; i++) { + position += this.viewItems[i].size; + + if (this.sashItems[i].sash === sash) { + return position; + } + } + + throw new Error('Sash not found'); + } + + dispose(): void { + } +} diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts new file mode 100644 index 00000000000..94dbb16fe87 --- /dev/null +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from 'vs/base/common/event'; +import { SplitView, IView, Orientation } from 'vs/base/browser/ui/splitview/splitview2'; + +class TestView implements IView { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + get minimumSize(): number { return this._minimumSize; } + set minimumSize(size: number) { this._minimumSize = size; this._onDidChange.fire(); } + + get maximumSize(): number { return this._maximumSize; } + set maximumSize(size: number) { this._maximumSize = size; this._onDidChange.fire(); } + + private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>(); + readonly onDidRender = this._onDidRender.event; + + private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); + readonly onDidLayout = this._onDidLayout.event; + + private _onDidFocus = new Emitter(); + readonly onDidFocus = this._onDidFocus.event; + + constructor( + private _minimumSize: number, + private _maximumSize: number + ) { + assert(_minimumSize <= _maximumSize, 'splitview view minimum size must be <= maximum size'); + } + + render(container: HTMLElement, orientation: Orientation): void { + this._onDidRender.fire({ container, orientation }); + } + + layout(size: number, orientation: Orientation): void { + this._onDidLayout.fire({ size, orientation }); + } + + focus(): void { + this._onDidFocus.fire(); + } + + dispose(): void { + this._onDidChange.dispose(); + this._onDidRender.dispose(); + this._onDidLayout.dispose(); + this._onDidFocus.dispose(); + } +} + +suite('Splitview', () => { + let container: HTMLElement; + + setup(() => { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = '200px'; + container.style.height = '200px'; + }); + + teardown(() => { + container = null; + }); + + test('empty splitview has empty DOM', () => { + const splitview = new SplitView(container); + assert.equal(container.firstElementChild.childElementCount, 0, 'split view should be empty'); + splitview.dispose(); + }); + + test('splitview has views as sashes as children', () => { + const view = new TestView(20, 20); + const splitview = new SplitView(container); + + splitview.addView(view, 20); + splitview.addView(view, 20); + splitview.addView(view, 20); + + let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 3, 'split view should have 3 views'); + + let sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 2, 'split view should have 2 sashes'); + + splitview.removeView(2); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 2, 'split view should have 2 views'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 1, 'split view should have 1 sash'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 1, 'split view should have 1 view'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 0, 'split view should have no views'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.dispose(); + view.dispose(); + }); +}); \ No newline at end of file From 9afefc8b00b63c20d6375b62a8044bc8f427ad31 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 17:13:53 +0200 Subject: [PATCH 08/67] arrays.range, arrays.weave --- src/vs/base/browser/ui/list/listWidget.ts | 2 +- src/vs/base/common/arrays.ts | 38 +++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 9eb101a4f35..cbba77216b3 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -398,7 +398,7 @@ class MouseController implements IDisposable { if (isSelectionRangeChangeEvent(e) && reference !== undefined) { const min = Math.min(reference, focus); const max = Math.max(reference, focus); - const rangeSelection = range(max + 1, min); + const rangeSelection = range(min, max + 1); const selection = this.list.getSelection(); const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 4be8d60f396..a64b84276eb 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -325,11 +325,43 @@ export function flatten(arr: T[][]): T[] { return arr.reduce((r, v) => r.concat(v), []); } -export function range(to: number, from = 0): number[] { +export function range(to: number): number[]; +export function range(from: number, to: number): number[]; +export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; + + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; + } + const result: number[] = []; - for (let i = from; i < to; i++) { - result.push(i); + if (from <= to) { + for (let i = from; i < to; i++) { + result.push(i); + } + } else { + for (let i = from; i > to; i--) { + result.push(i); + } + } + + return result; +} + +export function weave(a: T[], b: T[]): T[] { + const result: T[] = []; + let ai = 0, bi = 0; + + for (let i = 0, length = a.length + b.length; i < length; i++) { + if ((i % 2 === 0 && ai < a.length) || bi >= b.length) { + result.push(a[ai++]); + } else { + result.push(b[bi++]); + } } return result; From b34aba9078fa1a19a1a106db2d9609e62494aa69 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 23:20:52 +0200 Subject: [PATCH 09/67] more splitview tests --- .../base/browser/ui/splitview/splitview2.ts | 309 +++++++++++------- src/vs/base/common/numbers.ts | 5 + .../browser/ui/splitview/splitview.test.ts | 80 ++++- 3 files changed, 273 insertions(+), 121 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index a65ad774a40..3d357223596 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -6,10 +6,12 @@ 'use strict'; import 'vs/css!./splitview'; -import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; import types = require('vs/base/common/types'); import dom = require('vs/base/browser/dom'); +import { clamp } from 'vs/base/common/numbers'; +import { range, firstIndex, weave } from 'vs/base/common/arrays'; import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; @@ -33,33 +35,22 @@ export interface IView { focus(): void; } -class ViewItem { - - public explicitSize: number; - - constructor( - readonly view: IView, - readonly container: HTMLElement, - public size: number, - private orientation: Orientation, - private disposables: IDisposable[] - ) { +interface IViewItem { + view: IView; + size: number; + explicitSize: number; + container: HTMLElement; + disposable: IDisposable; +} +function layoutViewItem(item: IViewItem, orientation: Orientation): void { + if (orientation === Orientation.VERTICAL) { + item.container.style.height = `${item.size}px`; + } else { + item.container.style.width = `${item.size}px`; } - layout(): void { - if (this.orientation === Orientation.VERTICAL) { - this.container.style.height = `${this.size}px`; - } else { - this.container.style.width = `${this.size}px`; - } - - this.view.layout(this.size, this.orientation); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - } + item.view.layout(item.size, orientation); } interface ISashItem { @@ -67,14 +58,27 @@ interface ISashItem { disposable: IDisposable; } +interface ISashDragState { + start: number; + sizes: number[]; + up: number[]; + down: number[]; + maxUp: number; + maxDown: number; + collapses: number[]; + expands: number[]; +} + +const sum = (a: number[]) => a.reduce((a, b) => a + b, 0); + export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { private orientation: Orientation; private el: HTMLElement; - // private size: number; + private size = 0; // private viewElements: HTMLElement[]; - private viewItems: ViewItem[] = []; + private viewItems: IViewItem[] = []; private sashItems: ISashItem[] = []; // private viewChangeListeners: IDisposable[]; // private viewFocusPreviousListeners: IDisposable[]; @@ -86,7 +90,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // private sashesListeners: IDisposable[]; // private eventWrapper: (event: ISashEvent) => ISashEvent; // private animationTimeout: number; - // private state: IState; + private sashDragState: ISashDragState; // private _onFocus: Emitter = this._register(new Emitter()); // readonly onFocus: Event = this._onFocus.event; @@ -133,24 +137,26 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // this.addView(new VoidView(), 0); } - private getContainerSize(): number { - return this.orientation === Orientation.VERTICAL - ? dom.getContentHeight(this.container) - : dom.getContentWidth(this.container); - } + // private getContainerSize(): number { + // return this.orientation === Orientation.VERTICAL + // ? dom.getContentHeight(this.container) + // : dom.getContentWidth(this.container); + // } - addView(view: IView, size: number, index = this.viewItems.length - 1): void { + addView(view: IView, size: number, index = this.viewItems.length): void { // Create view container const container = document.createElement('div'); dom.addClass(container, 'split-view-view'); const containerDisposable = toDisposable(() => this.el.removeChild(container)); - // List to change events + // Create item + const item: IViewItem = { view, container, explicitSize: size, size, disposable: EmptyDisposable }; + this.viewItems.splice(index, 0, item); + const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); - // Create item - const item = new ViewItem(view, container, size, this.orientation, [onChangeDisposable, containerDisposable]); - this.viewItems.splice(index, 0, item); + // Disposable + item.disposable = combinedDisposable([onChangeDisposable, containerDisposable]); // Render view view.render(container, this.orientation); @@ -163,33 +169,33 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } // Add sash - if (this.viewItems.length <= 1) { - return; + if (this.viewItems.length > 1) { + const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; + const sash = new Sash(this.el, this, { orientation }); + const sashEventMapper = this.orientation === Orientation.VERTICAL + ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) + : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); + + const onStart = mapEvent(fromEventEmitter(sash, 'start'), sashEventMapper); + const onStartDisposable = onStart(this.onSashStart, this); + const onChange = mapEvent(fromEventEmitter(sash, 'change'), sashEventMapper); + const onSashChangeDisposable = onChange(this.onSashChange, this); + const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); + const sashItem: ISashItem = { sash, disposable }; + + this.sashItems.splice(index - 1, 0, sashItem); } - const orientation = this.orientation === Orientation.VERTICAL - ? Orientation.HORIZONTAL - : Orientation.VERTICAL; + // TODO: layout + // go through all viewitems, set their size to preferred size + // sum all sizes up + // run expandcollapse - const sash = new Sash(this.el, this, { orientation }); - const sashEventMapper = this.orientation === Orientation.VERTICAL - ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) - : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); + this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); - const onStart = mapEvent(fromEventEmitter(sash, 'start'), sashEventMapper); - const onStartDisposable = onStart(this.onSashStart, this); - - const onChange = mapEvent(fromEventEmitter(sash, 'change'), sashEventMapper); - const onSashChangeDisposable = onChange(this.onSashChange, this); - - const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); - - const sashItem: ISashItem = { - sash, - disposable - }; - - this.sashItems.splice(index - 1, 0, sashItem); + const previousSize = this.size; + this.size = this.viewItems.reduce((r, i) => r + i.size, 0); + this.layout(previousSize); } removeView(index: number): void { @@ -199,63 +205,153 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // Remove view const viewItem = this.viewItems.splice(index, 1)[0]; - viewItem.dispose(); + const collapse = viewItem.size; + viewItem.disposable.dispose(); - if (this.viewItems.length < 1) { + // Remove sash + if (this.viewItems.length >= 1) { + const sashIndex = Math.max(index - 1, 0); + const sashItem = this.sashItems.splice(sashIndex, 1)[0]; + sashItem.disposable.dispose(); + } + + // Layout views + const up = range(index - 1, -1); + const down = range(index, this.viewItems.length); + const indexes = weave(up, down); + const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + + this.expandCollapse(collapse, collapses, [], indexes, []); + } + + layout(size: number): void { + // size = size || this.getContainerSize(); + + // size = Math.max(size, this.viewItems.reduce((t, i) => t + i.view.minimumSize, 0)); + + + if (this.size === size) { return; } - // Remove sash - const sashIndex = Math.max(index - 1, 0); - const sashItem = this.sashItems.splice(sashIndex, 1)[0]; - sashItem.disposable.dispose(); + const indexes = range(this.viewItems.length - 1, -1); + const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); + const diff = Math.abs(this.size - size); + + if (size < this.size) { + this.expandCollapse(Math.min(diff, sum(collapses)), collapses, expands, indexes, []); + } else if (size > this.size) { + this.expandCollapse(Math.min(diff, sum(expands)), collapses, expands, [], indexes); + } + + this.size = size; } - layout(size?: number): void { - size = size || this.getContainerSize(); - } + private onSashStart({ sash, start }: ISashEvent): void { + const i = firstIndex(this.sashItems, item => item.sash === sash); + const sizes = this.viewItems.map(i => i.size); + const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - private onSashStart({ sash, start, current }: ISashEvent): void { + const up = range(i, -1); + const down = range(i + 1, this.viewItems.length); + + const collapsesUp = up.map(i => collapses[i]); + const collapsesDown = down.map(i => collapses[i]); + const expandsUp = up.map(i => expands[i]); + const expandsDown = down.map(i => expands[i]); + + const maxUp = Math.min(sum(collapsesUp), sum(expandsDown)); + const maxDown = Math.min(sum(expandsUp), sum(collapsesDown)); + + this.sashDragState = { start, sizes, up, down, maxUp, maxDown, collapses, expands }; } private onSashChange({ sash, start, current }: ISashEvent): void { + const diff = current - this.sashDragState.start; + if (diff < 0) { + this.expandCollapse(Math.min(-diff, this.sashDragState.maxUp), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.up, this.sashDragState.down); + } else { + this.expandCollapse(Math.min(diff, this.sashDragState.maxDown), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.down, this.sashDragState.up); + } + + this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size); } - // Main algorithm - // private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void { - // let totalCollapse = collapse; - // let totalExpand = totalCollapse; + private onViewChange(item: IViewItem): void { + const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - // collapseIndexes.forEach(i => { - // let collapse = Math.min(collapses[i], totalCollapse); - // totalCollapse -= collapse; - // this.views[i].size -= collapse; - // }); + if (size === item.size) { + return; + } - // expandIndexes.forEach(i => { - // let expand = Math.min(expands[i], totalExpand); - // totalExpand -= expand; - // this.views[i].size += expand; - // }); + // this could maybe use the same code than the addView() does + + // this.setupAnimation(); + + const index = this.viewItems.indexOf(item); + const diff = Math.abs(size - item.size); + + const up = range(index - 1, -1); + const down = range(index + 1, this.viewItems.length); + const downUp = down.concat(up); + + const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); + + + let collapse: number, collapseIndexes: number[], expandIndexes: number[]; + + if (size < item.size) { + collapse = Math.min(downUp.reduce((t, i) => t + expands[i], 0), diff); + collapseIndexes = [index]; + expandIndexes = downUp; + + } else { + collapse = Math.min(downUp.reduce((t, i) => t + collapses[i], 0), diff); + collapseIndexes = downUp; + expandIndexes = [index]; + } + + this.expandCollapse(collapse, collapses, expands, collapseIndexes, expandIndexes); + // this.layoutViews(); + } + + // private setupAnimation(): void { + // if (types.isNumber(this.animationTimeout)) { + // window.clearTimeout(this.animationTimeout); + // } + + // dom.addClass(this.el, 'animated'); + // this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200); // } - private getLastFlexibleViewIndex(exceptIndex: number = null): number { - // for (let i = this.views.length - 1; i >= 0; i--) { - // if (exceptIndex === i) { - // continue; - // } - // if (this.views[i].sizing === ViewSizing.Flexible) { - // return i; - // } - // } + // private clearAnimation(): void { + // this.animationTimeout = null; + // dom.removeClass(this.el, 'animated'); + // } - return -1; - } + // Main algorithm + private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void { + let totalCollapse = collapse; + let totalExpand = totalCollapse; - private layoutViews(): void { - this.viewItems.forEach(item => item.layout()); + collapseIndexes.forEach(i => { + let collapse = Math.min(collapses[i], totalCollapse); + totalCollapse -= collapse; + this.viewItems[i].size -= collapse; + }); + + expandIndexes.forEach(i => { + let expand = Math.min(expands[i], totalExpand); + totalExpand -= expand; + this.viewItems[i].size += expand; + }); + + this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); this.sashItems.forEach(item => item.sash.layout()); // Update sashes enablement @@ -281,23 +377,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // }); } - private onViewChange(view: ViewItem): void { - } - - // private setupAnimation(): void { - // if (types.isNumber(this.animationTimeout)) { - // window.clearTimeout(this.animationTimeout); - // } - - // dom.addClass(this.el, 'animated'); - // this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200); - // } - - // private clearAnimation(): void { - // this.animationTimeout = null; - // dom.removeClass(this.el, 'animated'); - // } - getVerticalSashLeft(sash: Sash): number { return this.getSashPosition(sash); } diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts index 65bcbbd7922..9f804fe6dd9 100644 --- a/src/vs/base/common/numbers.ts +++ b/src/vs/base/common/numbers.ts @@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] { return result; } + + +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} \ No newline at end of file diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 94dbb16fe87..60d531b6c41 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -21,6 +21,8 @@ class TestView implements IView { private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>(); readonly onDidRender = this._onDidRender.event; + private _size = 0; + get size(): number { return this._size; } private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); readonly onDidLayout = this._onDidLayout.event; @@ -39,6 +41,7 @@ class TestView implements IView { } layout(size: number, orientation: Orientation): void { + this._size = size; this._onDidLayout.fire({ size, orientation }); } @@ -54,14 +57,16 @@ class TestView implements IView { } } +const TOTAL_SIZE = 200; + suite('Splitview', () => { let container: HTMLElement; setup(() => { container = document.createElement('div'); container.style.position = 'absolute'; - container.style.width = '200px'; - container.style.height = '200px'; + container.style.width = `${TOTAL_SIZE}px`; + container.style.height = `${TOTAL_SIZE}px`; }); teardown(() => { @@ -75,12 +80,14 @@ suite('Splitview', () => { }); test('splitview has views as sashes as children', () => { - const view = new TestView(20, 20); + const view1 = new TestView(20, 20); + const view2 = new TestView(20, 20); + const view3 = new TestView(20, 20); const splitview = new SplitView(container); - splitview.addView(view, 20); - splitview.addView(view, 20); - splitview.addView(view, 20); + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); assert.equal(viewQuery.length, 3, 'split view should have 3 views'); @@ -113,6 +120,67 @@ suite('Splitview', () => { assert.equal(sashQuery.length, 0, 'split view should have no sashes'); splitview.dispose(); + view1.dispose(); + view2.dispose(); + view3.dispose(); + }); + + test('splitview calls view methods on addView and removeView', () => { + const view = new TestView(20, 20); + const splitview = new SplitView(container); + + let didLayout = false; + const layoutDisposable = view.onDidLayout(() => didLayout = true); + + let didRender = false; + const renderDisposable = view.onDidRender(() => didRender = true); + + splitview.addView(view, 20); + + assert.equal(view.size, 20, 'view has right size'); + assert(didLayout, 'layout was called'); + assert(didLayout, 'render was called'); + + splitview.dispose(); + layoutDisposable.dispose(); + renderDisposable.dispose(); view.dispose(); }); + + test('splitview stretches view to viewport', () => { + const view = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(TOTAL_SIZE); + + splitview.addView(view, 20); + assert.equal(view.size, TOTAL_SIZE, 'view was stretched'); + + splitview.dispose(); + view.dispose(); + }); + + test('splitview respects preferred sizes', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(TOTAL_SIZE); + + splitview.addView(view1, 20); + assert.equal(view1.size, TOTAL_SIZE, 'view1 was stretched'); + + splitview.addView(view2, 20); + assert.equal(view1.size, 20, 'view1 size was restored'); + assert.equal(view2.size, TOTAL_SIZE - 20, 'view2 was stretched'); + + splitview.addView(view3, 20); + assert.equal(view1.size, 20, 'view1 size was restored'); + assert.equal(view2.size, 20, 'view2 size was restored'); + assert.equal(view3.size, TOTAL_SIZE - 20 * 2, 'view3 was stretched'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); }); \ No newline at end of file From 47af16f20a0153be36db25de4808371b2a0e9e25 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 23:38:46 +0200 Subject: [PATCH 10/67] splitview tests --- .../base/browser/ui/splitview/splitview2.ts | 193 ++++++------------ .../browser/ui/splitview/splitview.test.ts | 17 +- 2 files changed, 79 insertions(+), 131 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 3d357223596..bee93a2e7cb 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -6,24 +6,17 @@ 'use strict'; import 'vs/css!./splitview'; -import { IDisposable, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; import types = require('vs/base/common/types'); import dom = require('vs/base/browser/dom'); import { clamp } from 'vs/base/common/numbers'; -import { range, firstIndex, weave } from 'vs/base/common/arrays'; +import { range, firstIndex } from 'vs/base/common/arrays'; import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; -interface ISashEvent { - sash: Sash; - start: number; - current: number; -} - export interface IOptions { orientation?: Orientation; // default Orientation.VERTICAL - canChangeOrderByDragAndDrop?: boolean; } export interface IView { @@ -35,6 +28,12 @@ export interface IView { focus(): void; } +interface ISashEvent { + sash: Sash; + start: number; + current: number; +} + interface IViewItem { view: IView; size: number; @@ -43,16 +42,6 @@ interface IViewItem { disposable: IDisposable; } -function layoutViewItem(item: IViewItem, orientation: Orientation): void { - if (orientation === Orientation.VERTICAL) { - item.container.style.height = `${item.size}px`; - } else { - item.container.style.width = `${item.size}px`; - } - - item.view.layout(item.size, orientation); -} - interface ISashItem { sash: Sash; disposable: IDisposable; @@ -71,33 +60,25 @@ interface ISashDragState { const sum = (a: number[]) => a.reduce((a, b) => a + b, 0); +function layoutViewItem(item: IViewItem, orientation: Orientation): void { + if (orientation === Orientation.VERTICAL) { + item.container.style.height = `${item.size}px`; + } else { + item.container.style.width = `${item.size}px`; + } + + item.view.layout(item.size, orientation); +} + export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { private orientation: Orientation; - private el: HTMLElement; private size = 0; - // private viewElements: HTMLElement[]; private viewItems: IViewItem[] = []; private sashItems: ISashItem[] = []; - // private viewChangeListeners: IDisposable[]; - // private viewFocusPreviousListeners: IDisposable[]; - // private viewFocusNextListeners: IDisposable[]; - // private viewFocusListeners: IDisposable[]; - // private viewDnDListeners: IDisposable[][]; - // private sashOrientation: Orientation; - // private sashes: Sash[]; - // private sashesListeners: IDisposable[]; - // private eventWrapper: (event: ISashEvent) => ISashEvent; - // private animationTimeout: number; private sashDragState: ISashDragState; - // private _onFocus: Emitter = this._register(new Emitter()); - // readonly onFocus: Event = this._onFocus.event; - - // private _onDidOrderChange: Emitter = this._register(new Emitter()); - // readonly onDidOrderChange: Event = this._onDidOrderChange.event; - get length(): number { return this.viewItems.length; } @@ -110,64 +91,26 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV dom.addClass(this.el, 'monaco-split-view'); dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal'); container.appendChild(this.el); - - // this.size = null; - // this.viewElements = []; - // this.views = []; - // this.viewChangeListeners = []; - // this.viewFocusPreviousListeners = []; - // this.viewFocusNextListeners = []; - // this.viewFocusListeners = []; - // this.viewDnDListeners = []; - // this.sashes = []; - // this.sashesListeners = []; - // this.animationTimeout = null; - - // this.sashOrientation = this.orientation === Orientation.VERTICAL - // ? Orientation.HORIZONTAL - // : Orientation.VERTICAL; - - // if (this.orientation === Orientation.VERTICAL) { - // this.eventWrapper = e => { return { start: e.startY, current: e.currentY }; }; - // } else { - // this.eventWrapper = e => { return { start: e.startX, current: e.currentX }; }; - // } - - // The void space exists to handle the case where all other views are fixed size - // this.addView(new VoidView(), 0); } - // private getContainerSize(): number { - // return this.orientation === Orientation.VERTICAL - // ? dom.getContentHeight(this.container) - // : dom.getContentWidth(this.container); - // } - addView(view: IView, size: number, index = this.viewItems.length): void { - // Create view container - const container = document.createElement('div'); - dom.addClass(container, 'split-view-view'); - const containerDisposable = toDisposable(() => this.el.removeChild(container)); + // Add view + const container = dom.$('.split-view-view'); - // Create item - const item: IViewItem = { view, container, explicitSize: size, size, disposable: EmptyDisposable }; - this.viewItems.splice(index, 0, item); - - const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); - - // Disposable - item.disposable = combinedDisposable([onChangeDisposable, containerDisposable]); - - // Render view - view.render(container, this.orientation); - - // Attach view if (this.viewItems.length === 1) { this.el.appendChild(container); } else { this.el.insertBefore(container, this.el.children.item(index)); } + const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); + const containerDisposable = toDisposable(() => this.el.removeChild(container)); + const disposable = combinedDisposable([onChangeDisposable, containerDisposable]); + + const explicitSize = size; + const item: IViewItem = { view, container, explicitSize, size, disposable }; + this.viewItems.splice(index, 0, item); + // Add sash if (this.viewItems.length > 1) { const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; @@ -186,16 +129,8 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV this.sashItems.splice(index - 1, 0, sashItem); } - // TODO: layout - // go through all viewitems, set their size to preferred size - // sum all sizes up - // run expandcollapse - - this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); - - const previousSize = this.size; - this.size = this.viewItems.reduce((r, i) => r + i.size, 0); - this.layout(previousSize); + view.render(container, this.orientation); + this.relayout(); } removeView(index: number): void { @@ -205,7 +140,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // Remove view const viewItem = this.viewItems.splice(index, 1)[0]; - const collapse = viewItem.size; viewItem.disposable.dispose(); // Remove sash @@ -215,21 +149,18 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV sashItem.disposable.dispose(); } - // Layout views - const up = range(index - 1, -1); - const down = range(index, this.viewItems.length); - const indexes = weave(up, down); - const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + this.relayout(); + } - this.expandCollapse(collapse, collapses, [], indexes, []); + private relayout(): void { + this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); + + const previousSize = this.size; + this.size = this.viewItems.reduce((r, i) => r + i.size, 0); + this.layout(previousSize); } layout(size: number): void { - // size = size || this.getContainerSize(); - - // size = Math.max(size, this.viewItems.reduce((t, i) => t + i.view.minimumSize, 0)); - - if (this.size === size) { return; } @@ -237,11 +168,12 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV const indexes = range(this.viewItems.length - 1, -1); const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - const diff = Math.abs(this.size - size); + const totalViewsSize = this.viewItems.reduce((r, i) => r + i.size, 0); + const diff = Math.abs(totalViewsSize - size); - if (size < this.size) { + if (size < totalViewsSize) { this.expandCollapse(Math.min(diff, sum(collapses)), collapses, expands, indexes, []); - } else if (size > this.size) { + } else if (size > totalViewsSize) { this.expandCollapse(Math.min(diff, sum(expands)), collapses, expands, [], indexes); } @@ -288,8 +220,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV return; } - // this could maybe use the same code than the addView() does - // this.setupAnimation(); const index = this.viewItems.indexOf(item); @@ -302,7 +232,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - let collapse: number, collapseIndexes: number[], expandIndexes: number[]; if (size < item.size) { @@ -317,7 +246,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } this.expandCollapse(collapse, collapses, expands, collapseIndexes, expandIndexes); - // this.layoutViews(); } // private setupAnimation(): void { @@ -355,26 +283,26 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV this.sashItems.forEach(item => item.sash.layout()); // Update sashes enablement - // let previous = false; - // let collapsesDown = this.views.map(v => previous = (v.size - v.minimumSize > 0) || previous); + let previous = false; + const collapsesDown = this.viewItems.map(i => previous = (i.size - i.view.minimumSize > 0) || previous); - // previous = false; - // let expandsDown = this.views.map(v => previous = (v.maximumSize - v.size > 0) || previous); + previous = false; + const expandsDown = this.viewItems.map(i => previous = (i.view.maximumSize - i.size > 0) || previous); - // let reverseViews = this.views.slice().reverse(); - // previous = false; - // let collapsesUp = reverseViews.map(v => previous = (v.size - v.minimumSize > 0) || previous).reverse(); + const reverseViews = [...this.viewItems].reverse(); + previous = false; + const collapsesUp = reverseViews.map(i => previous = (i.size - i.view.minimumSize > 0) || previous).reverse(); - // previous = false; - // let expandsUp = reverseViews.map(v => previous = (v.maximumSize - v.size > 0) || previous).reverse(); + previous = false; + const expandsUp = reverseViews.map(i => previous = (i.view.maximumSize - i.size > 0) || previous).reverse(); - // this.sashes.forEach((s, i) => { - // if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) { - // s.enable(); - // } else { - // s.disable(); - // } - // }); + this.sashItems.forEach((s, i) => { + if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) { + s.sash.enable(); + } else { + s.sash.disable(); + } + }); } getVerticalSashLeft(sash: Sash): number { @@ -400,5 +328,10 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } dispose(): void { + this.viewItems.forEach(i => i.disposable.dispose()); + this.viewItems = []; + + this.sashItems.forEach(i => i.disposable.dispose()); + this.sashItems = []; } } diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 60d531b6c41..8ae66da6710 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -155,11 +155,26 @@ suite('Splitview', () => { splitview.addView(view, 20); assert.equal(view.size, TOTAL_SIZE, 'view was stretched'); + splitview.layout(TOTAL_SIZE); + assert.equal(view.size, TOTAL_SIZE, 'view stayed the same'); + + splitview.layout(100); + assert.equal(view.size, 100, 'view was collapsed'); + + splitview.layout(20); + assert.equal(view.size, 20, 'view was collapsed'); + + splitview.layout(10); + assert.equal(view.size, 20, 'view was clamped'); + + splitview.layout(TOTAL_SIZE); + assert.equal(view.size, TOTAL_SIZE, 'view was stretched'); + splitview.dispose(); view.dispose(); }); - test('splitview respects preferred sizes', () => { + test('splitview respects preferred sizes with structural changes', () => { const view1 = new TestView(20, Number.POSITIVE_INFINITY); const view2 = new TestView(20, Number.POSITIVE_INFINITY); const view3 = new TestView(20, Number.POSITIVE_INFINITY); From e5aea8641129b7c5cdbd1053ec3874aa3eb2a225 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 15 Sep 2017 17:44:32 +0200 Subject: [PATCH 11/67] splitview: resizeView --- .../base/browser/ui/splitview/splitview2.ts | 38 +++++++-- .../browser/ui/splitview/splitview.test.ts | 77 ++++++++++++------- 2 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index bee93a2e7cb..7c151960b9a 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -49,7 +49,6 @@ interface ISashItem { interface ISashDragState { start: number; - sizes: number[]; up: number[]; down: number[]; maxUp: number; @@ -181,14 +180,12 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } private onSashStart({ sash, start }: ISashEvent): void { - const i = firstIndex(this.sashItems, item => item.sash === sash); - const sizes = this.viewItems.map(i => i.size); + const index = firstIndex(this.sashItems, item => item.sash === sash); const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - - const up = range(i, -1); - const down = range(i + 1, this.viewItems.length); + const up = range(index, -1); + const down = range(index + 1, this.viewItems.length); const collapsesUp = up.map(i => collapses[i]); const collapsesDown = down.map(i => collapses[i]); @@ -198,7 +195,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV const maxUp = Math.min(sum(collapsesUp), sum(expandsDown)); const maxDown = Math.min(sum(expandsUp), sum(collapsesDown)); - this.sashDragState = { start, sizes, up, down, maxUp, maxDown, collapses, expands }; + this.sashDragState = { start, up, down, maxUp, maxDown, collapses, expands }; } private onSashChange({ sash, start, current }: ISashEvent): void { @@ -213,6 +210,33 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size); } + resizeView(index: number, size: number): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + const viewItem = this.viewItems[index]; + size = clamp(size, viewItem.view.minimumSize, viewItem.view.maximumSize); + + const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); + const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); + const up = range(index, -1); + const down = range(index + 1, this.viewItems.length); + const collapsesUp = up.map(i => collapses[i]); + const collapsesDown = down.map(i => collapses[i]); + const expandsUp = up.map(i => expands[i]); + const expandsDown = down.map(i => expands[i]); + const maxUp = Math.min(sum(collapsesUp), sum(expandsDown)); + const maxDown = Math.min(sum(expandsUp), sum(collapsesDown)); + const diff = size - viewItem.size; + + if (diff < 0) { + this.expandCollapse(Math.min(-diff, maxUp), collapses, expands, up, down); + } else { + this.expandCollapse(Math.min(diff, maxDown), collapses, expands, down, up); + } + } + private onViewChange(item: IViewItem): void { const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 8ae66da6710..661efa84f50 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -57,16 +57,14 @@ class TestView implements IView { } } -const TOTAL_SIZE = 200; - suite('Splitview', () => { let container: HTMLElement; setup(() => { container = document.createElement('div'); container.style.position = 'absolute'; - container.style.width = `${TOTAL_SIZE}px`; - container.style.height = `${TOTAL_SIZE}px`; + container.style.width = `${200}px`; + container.style.height = `${200}px`; }); teardown(() => { @@ -79,7 +77,7 @@ suite('Splitview', () => { splitview.dispose(); }); - test('splitview has views as sashes as children', () => { + test('has views as sashes as children', () => { const view1 = new TestView(20, 20); const view2 = new TestView(20, 20); const view3 = new TestView(20, 20); @@ -125,7 +123,7 @@ suite('Splitview', () => { view3.dispose(); }); - test('splitview calls view methods on addView and removeView', () => { + test('calls view methods on addView and removeView', () => { const view = new TestView(20, 20); const splitview = new SplitView(container); @@ -138,8 +136,8 @@ suite('Splitview', () => { splitview.addView(view, 20); assert.equal(view.size, 20, 'view has right size'); - assert(didLayout, 'layout was called'); - assert(didLayout, 'render was called'); + assert(didLayout, 'layout is called'); + assert(didLayout, 'render is called'); splitview.dispose(); layoutDisposable.dispose(); @@ -147,51 +145,78 @@ suite('Splitview', () => { view.dispose(); }); - test('splitview stretches view to viewport', () => { + test('stretches view to viewport', () => { const view = new TestView(20, Number.POSITIVE_INFINITY); const splitview = new SplitView(container); - splitview.layout(TOTAL_SIZE); + splitview.layout(200); splitview.addView(view, 20); - assert.equal(view.size, TOTAL_SIZE, 'view was stretched'); + assert.equal(view.size, 200, 'view is stretched'); - splitview.layout(TOTAL_SIZE); - assert.equal(view.size, TOTAL_SIZE, 'view stayed the same'); + splitview.layout(200); + assert.equal(view.size, 200, 'view stayed the same'); splitview.layout(100); - assert.equal(view.size, 100, 'view was collapsed'); + assert.equal(view.size, 100, 'view is collapsed'); splitview.layout(20); - assert.equal(view.size, 20, 'view was collapsed'); + assert.equal(view.size, 20, 'view is collapsed'); splitview.layout(10); - assert.equal(view.size, 20, 'view was clamped'); + assert.equal(view.size, 20, 'view is clamped'); - splitview.layout(TOTAL_SIZE); - assert.equal(view.size, TOTAL_SIZE, 'view was stretched'); + splitview.layout(200); + assert.equal(view.size, 200, 'view is stretched'); splitview.dispose(); view.dispose(); }); - test('splitview respects preferred sizes with structural changes', () => { + test('respects preferred sizes with structural changes', () => { const view1 = new TestView(20, Number.POSITIVE_INFINITY); const view2 = new TestView(20, Number.POSITIVE_INFINITY); const view3 = new TestView(20, Number.POSITIVE_INFINITY); const splitview = new SplitView(container); - splitview.layout(TOTAL_SIZE); + splitview.layout(200); splitview.addView(view1, 20); - assert.equal(view1.size, TOTAL_SIZE, 'view1 was stretched'); + assert.equal(view1.size, 200, 'view1 is stretched'); splitview.addView(view2, 20); - assert.equal(view1.size, 20, 'view1 size was restored'); - assert.equal(view2.size, TOTAL_SIZE - 20, 'view2 was stretched'); + assert.equal(view1.size, 20, 'view1 size is restored'); + assert.equal(view2.size, 200 - 20, 'view2 is stretched'); splitview.addView(view3, 20); - assert.equal(view1.size, 20, 'view1 size was restored'); - assert.equal(view2.size, 20, 'view2 size was restored'); - assert.equal(view3.size, TOTAL_SIZE - 20 * 2, 'view3 was stretched'); + assert.equal(view1.size, 20, 'view1 size is restored'); + assert.equal(view2.size, 20, 'view2 size is restored'); + assert.equal(view3.size, 160, 'view3 is stretched'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); + + test('can resize views', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + assert.equal(view1.size, 20, 'view1 size is the default'); + assert.equal(view2.size, 20, 'view2 size the the default'); + assert.equal(view3.size, 160, 'view3 is stretched'); + + splitview.resizeView(1, 40); + + assert.equal(view1.size, 20, 'view1 is untouched'); + assert.equal(view2.size, 40, 'view2 is stretched'); + assert.equal(view3.size, 140, 'view3 is collapsed'); splitview.dispose(); view3.dispose(); From 806b6c7e88219f8d614ced8102f06ad8dba3f293 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 15 Sep 2017 17:51:58 +0200 Subject: [PATCH 12/67] splitview: more resizeView tests --- .../base/test/browser/ui/splitview/splitview.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 661efa84f50..0dd18c99bf7 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -218,6 +218,18 @@ suite('Splitview', () => { assert.equal(view2.size, 40, 'view2 is stretched'); assert.equal(view3.size, 140, 'view3 is collapsed'); + splitview.resizeView(0, 70); + + assert.equal(view1.size, 70, 'view1 is stretched'); + assert.equal(view2.size, 20, 'view2 is collapsed'); + assert.equal(view3.size, 110, 'view3 is collapsed'); + + splitview.resizeView(2, 20); + + assert.equal(view1.size, 70, 'view1 is stretched'); + assert.equal(view2.size, 110, 'view2 is stretched'); + assert.equal(view3.size, 20, 'view3 is collapsed only to minimum size'); + splitview.dispose(); view3.dispose(); view2.dispose(); From a6af9d66711917e8c18100a1e1b029bd656fbffb Mon Sep 17 00:00:00 2001 From: Alex Cao Date: Fri, 15 Sep 2017 11:40:16 -0700 Subject: [PATCH 13/67] Improve search aria label generation performance --- src/vs/workbench/parts/search/browser/searchResultsView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index 752b44681bc..20a3086d064 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -310,12 +310,12 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider { const match = element; const searchModel: SearchModel = (tree.getInput()).searchModel; const replace = searchModel.isReplaceActive() && !!searchModel.replaceString; - const preview = match.preview(); + const matchString = match.getMatchString(); const range = match.range(); if (replace) { - return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", preview.inside, match.replaceString, range.startColumn + 1, match.text()); + return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, match.text()); } - return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", preview.inside, range.startColumn + 1, match.text()); + return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, match.text()); } return undefined; } From 56ee834f60bd179a89d029e7875da3a4f95d73f2 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sat, 16 Sep 2017 15:43:58 +0200 Subject: [PATCH 14/67] splitview: cleaner main algorithm --- .../base/browser/ui/splitview/splitview2.ts | 165 +++++------------- .../browser/ui/splitview/splitview.test.ts | 8 +- 2 files changed, 51 insertions(+), 122 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 7c151960b9a..854517269a1 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -48,17 +48,11 @@ interface ISashItem { } interface ISashDragState { + index: number; start: number; - up: number[]; - down: number[]; - maxUp: number; - maxDown: number; - collapses: number[]; - expands: number[]; + sizes: number[]; } -const sum = (a: number[]) => a.reduce((a, b) => a + b, 0); - function layoutViewItem(item: IViewItem, orientation: Orientation): void { if (orientation === Orientation.VERTICAL) { item.container.style.height = `${item.size}px`; @@ -160,116 +154,29 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } layout(size: number): void { - if (this.size === size) { - return; - } - - const indexes = range(this.viewItems.length - 1, -1); - const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); - const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - const totalViewsSize = this.viewItems.reduce((r, i) => r + i.size, 0); - const diff = Math.abs(totalViewsSize - size); - - if (size < totalViewsSize) { - this.expandCollapse(Math.min(diff, sum(collapses)), collapses, expands, indexes, []); - } else if (size > totalViewsSize) { - this.expandCollapse(Math.min(diff, sum(expands)), collapses, expands, [], indexes); - } - - this.size = size; + this.resize(this.viewItems.length - 1, size - this.size); + this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0)); } private onSashStart({ sash, start }: ISashEvent): void { const index = firstIndex(this.sashItems, item => item.sash === sash); - const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); - const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); + const sizes = this.viewItems.map(i => i.size); - const up = range(index, -1); - const down = range(index + 1, this.viewItems.length); - - const collapsesUp = up.map(i => collapses[i]); - const collapsesDown = down.map(i => collapses[i]); - const expandsUp = up.map(i => expands[i]); - const expandsDown = down.map(i => expands[i]); - - const maxUp = Math.min(sum(collapsesUp), sum(expandsDown)); - const maxDown = Math.min(sum(expandsUp), sum(collapsesDown)); - - this.sashDragState = { start, up, down, maxUp, maxDown, collapses, expands }; + this.sashDragState = { start, index, sizes }; } - private onSashChange({ sash, start, current }: ISashEvent): void { - const diff = current - this.sashDragState.start; - - if (diff < 0) { - this.expandCollapse(Math.min(-diff, this.sashDragState.maxUp), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.up, this.sashDragState.down); - } else { - this.expandCollapse(Math.min(diff, this.sashDragState.maxDown), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.down, this.sashDragState.up); - } + private onSashChange({ sash, current }: ISashEvent): void { + const { index, start, sizes } = this.sashDragState; + this.resize(index, current - start, sizes); this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size); } - resizeView(index: number, size: number): void { - if (index < 0 || index >= this.viewItems.length) { - return; - } - - const viewItem = this.viewItems[index]; - size = clamp(size, viewItem.view.minimumSize, viewItem.view.maximumSize); - - const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); - const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - const up = range(index, -1); - const down = range(index + 1, this.viewItems.length); - const collapsesUp = up.map(i => collapses[i]); - const collapsesDown = down.map(i => collapses[i]); - const expandsUp = up.map(i => expands[i]); - const expandsDown = down.map(i => expands[i]); - const maxUp = Math.min(sum(collapsesUp), sum(expandsDown)); - const maxDown = Math.min(sum(expandsUp), sum(collapsesDown)); - const diff = size - viewItem.size; - - if (diff < 0) { - this.expandCollapse(Math.min(-diff, maxUp), collapses, expands, up, down); - } else { - this.expandCollapse(Math.min(diff, maxDown), collapses, expands, down, up); - } - } - private onViewChange(item: IViewItem): void { + const index = this.viewItems.indexOf(item); const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - if (size === item.size) { - return; - } - - // this.setupAnimation(); - - const index = this.viewItems.indexOf(item); - const diff = Math.abs(size - item.size); - - const up = range(index - 1, -1); - const down = range(index + 1, this.viewItems.length); - const downUp = down.concat(up); - - const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0)); - const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0)); - - let collapse: number, collapseIndexes: number[], expandIndexes: number[]; - - if (size < item.size) { - collapse = Math.min(downUp.reduce((t, i) => t + expands[i], 0), diff); - collapseIndexes = [index]; - expandIndexes = downUp; - - } else { - collapse = Math.min(downUp.reduce((t, i) => t + collapses[i], 0), diff); - collapseIndexes = downUp; - expandIndexes = [index]; - } - - this.expandCollapse(collapse, collapses, expands, collapseIndexes, expandIndexes); + this.resize(index, size - item.size); } // private setupAnimation(): void { @@ -286,22 +193,44 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // dom.removeClass(this.el, 'animated'); // } - // Main algorithm - private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void { - let totalCollapse = collapse; - let totalExpand = totalCollapse; + resizeView(index: number, size: number): void { + if (index < 0 || index >= this.viewItems.length - 1) { + throw new Error('Cant resize view'); + } - collapseIndexes.forEach(i => { - let collapse = Math.min(collapses[i], totalCollapse); - totalCollapse -= collapse; - this.viewItems[i].size -= collapse; - }); + this.resize(index, size - this.viewItems[index].size); + } - expandIndexes.forEach(i => { - let expand = Math.min(expands[i], totalExpand); - totalExpand -= expand; - this.viewItems[i].size += expand; - }); + private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void { + if (delta === 0 || index < 0 || index >= this.viewItems.length) { + return; + } + + const upIndexes = range(index, -1); + const up = upIndexes.map(i => this.viewItems[i]); + const upSizes = upIndexes.map(i => sizes[i]); + + const downIndexes = range(index + 1, this.viewItems.length); + const down = downIndexes.map(i => this.viewItems[i]); + const downSizes = downIndexes.map(i => sizes[i]); + + for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { + const item = up[i]; + const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - upSizes[i]; + + deltaUp -= viewDelta; + item.size = size; + } + + for (let i = 0, deltaDown = delta; deltaDown !== 0 && i < down.length; i++) { + const item = down[i]; + const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - downSizes[i]; + + deltaDown += viewDelta; + item.size = size; + } this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); this.sashItems.forEach(item => item.sash.layout()); diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 0dd18c99bf7..eb324220b85 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -224,11 +224,11 @@ suite('Splitview', () => { assert.equal(view2.size, 20, 'view2 is collapsed'); assert.equal(view3.size, 110, 'view3 is collapsed'); - splitview.resizeView(2, 20); + assert.throws(() => splitview.resizeView(2, 20)); - assert.equal(view1.size, 70, 'view1 is stretched'); - assert.equal(view2.size, 110, 'view2 is stretched'); - assert.equal(view3.size, 20, 'view3 is collapsed only to minimum size'); + assert.equal(view1.size, 70, 'view1 stays the same'); + assert.equal(view2.size, 20, 'view2 stays the same'); + assert.equal(view3.size, 110, 'view3 stays the same'); splitview.dispose(); view3.dispose(); From 83aa0b1244366dcd82ae944c53edc618c84d081d Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 09:18:40 +0200 Subject: [PATCH 15/67] splitview: more tests --- src/vs/base/browser/ui/sash/sash.ts | 4 + .../base/browser/ui/splitview/splitview2.ts | 75 ++++++------- .../browser/ui/splitview/splitview.test.ts | 100 ++++++++++++++++++ 3 files changed, 143 insertions(+), 36 deletions(-) diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 338d07b4681..4fdafd509a4 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -268,6 +268,10 @@ export class Sash extends EventEmitter { this.isDisabled = true; } + get enabled(): boolean { + return !this.isDisabled; + } + public dispose(): void { if (this.$e) { this.$e.destroy(); diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 854517269a1..ed91fe55aa4 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -123,7 +123,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } view.render(container, this.orientation); - this.relayout(); + this.relayoutPreferredSizes(); } removeView(index: number): void { @@ -142,15 +142,12 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV sashItem.disposable.dispose(); } - this.relayout(); + this.relayoutPreferredSizes(); } - private relayout(): void { + private relayoutPreferredSizes(): void { this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); - - const previousSize = this.size; - this.size = this.viewItems.reduce((r, i) => r + i.size, 0); - this.layout(previousSize); + this.relayout(); } layout(size: number): void { @@ -158,6 +155,12 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0)); } + private relayout(): void { + const previousSize = this.size; + this.size = this.viewItems.reduce((r, i) => r + i.size, 0); + this.layout(previousSize); + } + private onSashStart({ sash, start }: ISashEvent): void { const index = firstIndex(this.sashItems, item => item.sash === sash); const sizes = this.viewItems.map(i => i.size); @@ -173,10 +176,8 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } private onViewChange(item: IViewItem): void { - const index = this.viewItems.indexOf(item); - const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - - this.resize(index, size - item.size); + item.size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); + this.relayout(); } // private setupAnimation(): void { @@ -202,39 +203,41 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV } private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void { - if (delta === 0 || index < 0 || index >= this.viewItems.length) { + if (index < 0 || index >= this.viewItems.length) { return; } - const upIndexes = range(index, -1); - const up = upIndexes.map(i => this.viewItems[i]); - const upSizes = upIndexes.map(i => sizes[i]); + if (delta !== 0) { + const upIndexes = range(index, -1); + const up = upIndexes.map(i => this.viewItems[i]); + const upSizes = upIndexes.map(i => sizes[i]); - const downIndexes = range(index + 1, this.viewItems.length); - const down = downIndexes.map(i => this.viewItems[i]); - const downSizes = downIndexes.map(i => sizes[i]); + const downIndexes = range(index + 1, this.viewItems.length); + const down = downIndexes.map(i => this.viewItems[i]); + const downSizes = downIndexes.map(i => sizes[i]); - for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { - const item = up[i]; - const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); - const viewDelta = size - upSizes[i]; + for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { + const item = up[i]; + const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - upSizes[i]; - deltaUp -= viewDelta; - item.size = size; + deltaUp -= viewDelta; + item.size = size; + } + + for (let i = 0, deltaDown = delta; deltaDown !== 0 && i < down.length; i++) { + const item = down[i]; + const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - downSizes[i]; + + deltaDown += viewDelta; + item.size = size; + } + + this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); + this.sashItems.forEach(item => item.sash.layout()); } - for (let i = 0, deltaDown = delta; deltaDown !== 0 && i < down.length; i++) { - const item = down[i]; - const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize); - const viewDelta = size - downSizes[i]; - - deltaDown += viewDelta; - item.size = size; - } - - this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); - this.sashItems.forEach(item => item.sash.layout()); - // Update sashes enablement let previous = false; const collapsesDown = this.viewItems.map(i => previous = (i.size - i.view.minimumSize > 0) || previous); diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index eb324220b85..be73ee96d46 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { SplitView, IView, Orientation } from 'vs/base/browser/ui/splitview/splitview2'; +import { Sash } from 'vs/base/browser/ui/sash/sash'; class TestView implements IView { @@ -57,6 +58,10 @@ class TestView implements IView { } } +function getSashes(splitview: SplitView): Sash[] { + return (splitview as any).sashItems.map(i => i.sash) as Sash[]; +} + suite('Splitview', () => { let container: HTMLElement; @@ -235,4 +240,99 @@ suite('Splitview', () => { view2.dispose(); view1.dispose(); }); + + test('reacts to view changes', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + assert.equal(view1.size, 20, 'view1 size is restored'); + assert.equal(view2.size, 20, 'view2 size is restored'); + assert.equal(view3.size, 160, 'view3 is stretched'); + + view3.maximumSize = 20; + + assert.equal(view1.size, 20, 'view1 stays the same'); + assert.equal(view2.size, 160, 'view2 is stretched'); + assert.equal(view3.size, 20, 'view3 is collapsed'); + + view2.maximumSize = 40; + + assert.equal(view1.size, 140, 'view1 is stretched'); + assert.equal(view2.size, 40, 'view2 is collapsed'); + assert.equal(view3.size, 20, 'view3 is collapsed'); + + view3.maximumSize = 200; + + assert.equal(view1.size, 140, 'view1 stays the same'); + assert.equal(view2.size, 40, 'view2 stays the same'); + assert.equal(view3.size, 20, 'view3 stays the same'); + + view3.minimumSize = 100; + + assert.equal(view1.size, 80, 'view1 is collapsed'); + assert.equal(view2.size, 20, 'view2 stays the same'); + assert.equal(view3.size, 100, 'view3 is stretched'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); + + test('sashes are properly enabled/disabled', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + let sashes = getSashes(splitview); + assert.equal(sashes.length, 2, 'there are two sashes'); + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + splitview.layout(60); + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + splitview.layout(20); + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + splitview.layout(200); + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view1.maximumSize = 20; + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view2.maximumSize = 20; + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + view1.maximumSize = 300; + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view2.maximumSize = 200; + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); }); \ No newline at end of file From c2cc2066afb4a3ad9109d65b7007b2803aa47f57 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 09:24:18 +0200 Subject: [PATCH 16/67] splitview: animation --- .../base/browser/ui/splitview/splitview2.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index ed91fe55aa4..bfa0ef0eabb 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -71,6 +71,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV private viewItems: IViewItem[] = []; private sashItems: ISashItem[] = []; private sashDragState: ISashDragState; + private animationTimeout: number; get length(): number { return this.viewItems.length; @@ -150,17 +151,17 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV this.relayout(); } - layout(size: number): void { - this.resize(this.viewItems.length - 1, size - this.size); - this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0)); - } - private relayout(): void { const previousSize = this.size; this.size = this.viewItems.reduce((r, i) => r + i.size, 0); this.layout(previousSize); } + layout(size: number): void { + this.resize(this.viewItems.length - 1, size - this.size); + this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0)); + } + private onSashStart({ sash, start }: ISashEvent): void { const index = firstIndex(this.sashItems, item => item.sash === sash); const sizes = this.viewItems.map(i => i.size); @@ -177,22 +178,24 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV private onViewChange(item: IViewItem): void { item.size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); + this.setupAnimation(); this.relayout(); } - // private setupAnimation(): void { - // if (types.isNumber(this.animationTimeout)) { - // window.clearTimeout(this.animationTimeout); - // } + // TODO@Joao: move this to panelview + private setupAnimation(): void { + // Setup animation + if (types.isNumber(this.animationTimeout)) { + window.clearTimeout(this.animationTimeout); + } - // dom.addClass(this.el, 'animated'); - // this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200); - // } + dom.addClass(this.el, 'animated'); - // private clearAnimation(): void { - // this.animationTimeout = null; - // dom.removeClass(this.el, 'animated'); - // } + this.animationTimeout = window.setTimeout(() => { + this.animationTimeout = null; + dom.removeClass(this.el, 'animated'); + }, 200); + } resizeView(index: number, size: number): void { if (index < 0 || index >= this.viewItems.length - 1) { From ac47a3329140548935e74ebbfe1af8554bb53982 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 09:32:20 +0200 Subject: [PATCH 17/67] splitview: clean interface --- src/vs/base/browser/ui/splitview/splitview2.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index bfa0ef0eabb..053f144cf51 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -12,7 +12,7 @@ import types = require('vs/base/common/types'); import dom = require('vs/base/browser/dom'); import { clamp } from 'vs/base/common/numbers'; import { range, firstIndex } from 'vs/base/common/arrays'; -import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; +import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; export interface IOptions { @@ -63,7 +63,7 @@ function layoutViewItem(item: IViewItem, orientation: Orientation): void { item.view.layout(item.size, orientation); } -export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { +export class SplitView implements IDisposable { private orientation: Orientation; private el: HTMLElement; @@ -108,7 +108,8 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV // Add sash if (this.viewItems.length > 1) { const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; - const sash = new Sash(this.el, this, { orientation }); + const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: sash => this.getSashPosition(sash) } : { getVerticalSashLeft: sash => this.getSashPosition(sash) }; + const sash = new Sash(this.el, layoutProvider, { orientation }); const sashEventMapper = this.orientation === Orientation.VERTICAL ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); @@ -264,14 +265,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV }); } - getVerticalSashLeft(sash: Sash): number { - return this.getSashPosition(sash); - } - - getHorizontalSashTop(sash: Sash): number { - return this.getSashPosition(sash); - } - private getSashPosition(sash: Sash): number { let position = 0; From 1bf3150b67c7b46240bf5991c4856630988cdf6f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 10:05:21 +0200 Subject: [PATCH 18/67] panelview: first steps --- src/vs/base/browser/ui/splitview/panelview.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/vs/base/browser/ui/splitview/panelview.ts diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts new file mode 100644 index 00000000000..1603a64e3a9 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./splitview'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import Event, { Emitter, chain } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { $, append, toggleClass } from 'vs/base/browser/dom'; +import { IOptions, SplitView, IView } from './splitview2'; +export { IOptions } from './splitview2'; + +enum PanelState { + Expanded, + Collapsed +} + +export interface IPanelOptions { + ariaHeaderLabel?: string; + minimumBodySize?: number; + maximumBodySize?: number; + collapsed?: boolean; +} + +export abstract class Panel implements IView { + + private static HEADER_SIZE = 22; + + private state: PanelState = PanelState.Expanded; + private _onDidChange = new Emitter(); + private _minimumBodySize: number; + private _maximumBodySize: number; + private ariaHeaderLabel: string; + private header: HTMLElement; + private body: HTMLElement; + private disposables: IDisposable[] = []; + + get minimumBodySize(): number { + return this._minimumBodySize; + } + + set minimumBodySize(size: number) { + this._minimumBodySize = size; + this._onDidChange.fire(); + } + + get maximumBodySize(): number { + return this._maximumBodySize; + } + + set maximumBodySize(size: number) { + this._maximumBodySize = size; + this._onDidChange.fire(); + } + + get minimumSize(): number { + return Panel.HEADER_SIZE + (this.state === PanelState.Collapsed ? 0 : this._minimumBodySize); + } + + get maximumSize(): number { + return Panel.HEADER_SIZE + (this.state === PanelState.Collapsed ? 0 : this._maximumBodySize); + } + + readonly onDidChange: Event = this._onDidChange.event; + + constructor(options: IPanelOptions = {}) { + this.ariaHeaderLabel = options.ariaHeaderLabel || ''; + this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44; + this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; + this.state = options.collapsed ? PanelState.Collapsed : PanelState.Expanded; + } + + render(container: HTMLElement): void { + const panel = append(container, $('.panel')); + const header = append(panel, $('.panel-header')); + + header.setAttribute('tabindex', '0'); + header.setAttribute('role', 'toolbar'); + header.setAttribute('aria-label', this.ariaHeaderLabel); + this.renderHeader(); + + const onHeaderKeyDown = chain(domEvent(header, 'keydown')).map(e => new StandardKeyboardEvent(e)); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) + .event(this.toggleExpansion, this, this.disposables); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) + .event(this.collapse, this, this.disposables); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) + .event(this.expand, this, this.disposables); + + // TODO@Joao move this down to panelview + // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow) + // .event(focusPrevious, this, this.disposables); + + // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow) + // .event(focusNext, this, this.disposables); + + this.body = append(panel, $('.panel-body')); + } + + private renderHeader(): void { + toggleClass(this.header, 'expanded', this.state === PanelState.Expanded); + this.header.setAttribute('aria-expanded', String(this.state === PanelState.Expanded)); + } + + layout(size: number): void { + this.layoutBody(size - Panel.HEADER_SIZE); + } + + focus(): void { + + } + + toggleExpansion(): void { + if (this.state === PanelState.Expanded) { + return this.collapse(); + } else { + return this.expand(); + } + } + + expand(): void { + if (this.state === PanelState.Expanded) { + return; + } + + this.renderHeader(); + } + + collapse(): void { + if (this.state === PanelState.Collapsed) { + return; + } + + this.renderHeader(); + } + + protected abstract renderBody(container: HTMLElement): void; + protected abstract layoutBody(size: number): void; + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export class PanelView implements IDisposable { + + private splitview: SplitView; + + constructor(private container: HTMLElement, options?: IOptions) { + this.splitview = new SplitView(container, options); + } + + addPanel(): void { + + } + + removePanel(): void { + + } + + layout(size: number): void { + + } + + dispose(): void { + } +} From 41dd25755f2c6d8b522cd69e5c8a3c87d9027fc2 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 16:15:12 +0200 Subject: [PATCH 19/67] panelview: animation, collapse states --- src/vs/base/browser/ui/splitview/panelview.ts | 77 +++++++++++++++---- .../base/browser/ui/splitview/splitview2.ts | 21 +---- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 1603a64e3a9..157be8261dc 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -11,9 +11,9 @@ import Event, { Emitter, chain } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { $, append, toggleClass } from 'vs/base/browser/dom'; -import { IOptions, SplitView, IView } from './splitview2'; -export { IOptions } from './splitview2'; +import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { firstIndex } from 'vs/base/common/arrays'; +import { SplitView, IView } from './splitview2'; enum PanelState { Expanded, @@ -37,7 +37,6 @@ export abstract class Panel implements IView { private _maximumBodySize: number; private ariaHeaderLabel: string; private header: HTMLElement; - private body: HTMLElement; private disposables: IDisposable[] = []; get minimumBodySize(): number { @@ -84,7 +83,8 @@ export abstract class Panel implements IView { header.setAttribute('aria-label', this.ariaHeaderLabel); this.renderHeader(); - const onHeaderKeyDown = chain(domEvent(header, 'keydown')).map(e => new StandardKeyboardEvent(e)); + const onHeaderKeyDown = chain(domEvent(header, 'keydown')) + .map(e => new StandardKeyboardEvent(e)); onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) .event(this.toggleExpansion, this, this.disposables); @@ -95,6 +95,8 @@ export abstract class Panel implements IView { onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) .event(this.expand, this, this.disposables); + domEvent(header, 'click')(this.toggleExpansion, this, this.disposables); + // TODO@Joao move this down to panelview // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow) // .event(focusPrevious, this, this.disposables); @@ -102,12 +104,8 @@ export abstract class Panel implements IView { // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow) // .event(focusNext, this, this.disposables); - this.body = append(panel, $('.panel-body')); - } - - private renderHeader(): void { - toggleClass(this.header, 'expanded', this.state === PanelState.Expanded); - this.header.setAttribute('aria-expanded', String(this.state === PanelState.Expanded)); + const body = append(panel, $('.panel-body')); + this.renderBody(body); } layout(size: number): void { @@ -115,7 +113,7 @@ export abstract class Panel implements IView { } focus(): void { - + // TODO@joao what to do } toggleExpansion(): void { @@ -132,6 +130,7 @@ export abstract class Panel implements IView { } this.renderHeader(); + this._onDidChange.fire(); } collapse(): void { @@ -140,6 +139,12 @@ export abstract class Panel implements IView { } this.renderHeader(); + this._onDidChange.fire(); + } + + private renderHeader(): void { + toggleClass(this.header, 'expanded', this.state === PanelState.Expanded); + this.header.setAttribute('aria-expanded', String(this.state === PanelState.Expanded)); } protected abstract renderBody(container: HTMLElement): void; @@ -150,26 +155,66 @@ export abstract class Panel implements IView { } } +export class IPanelViewOptions { + dnd?: boolean; +} + +interface IPanelItem { + panel: Panel; + disposable: IDisposable; +} + export class PanelView implements IDisposable { + private el: HTMLElement; + private panelItems: IPanelItem[] = []; private splitview: SplitView; + private animationTimer: number | null = null; - constructor(private container: HTMLElement, options?: IOptions) { - this.splitview = new SplitView(container, options); + constructor(private container: HTMLElement, options?: IPanelViewOptions) { + this.el = append(container, $('.monaco-panel-view')); + this.splitview = new SplitView(container); } - addPanel(): void { + addPanel(panel: Panel, size: number, index = this.splitview.length): void { + const disposable = panel.onDidChange(this.setupAnimation, this); + const panelItem = { panel, disposable }; + this.panelItems.splice(index, 0, panelItem); + this.splitview.addView(panel, size, index); } - removePanel(): void { + removePanel(panel: Panel): void { + const index = firstIndex(this.panelItems, item => item.panel === panel); + if (index === -1) { + return; + } + + this.splitview.removeView(index); + const panelItem = this.panelItems.splice(index, 1)[0]; + panelItem.disposable.dispose(); } layout(size: number): void { + this.splitview.layout(size); + } + // TODO@Joao: move this to panelview + private setupAnimation(): void { + if (typeof this.animationTimer === 'number') { + window.clearTimeout(this.animationTimer); + } + + addClass(this.el, 'animated'); + + this.animationTimer = window.setTimeout(() => { + this.animationTimer = null; + removeClass(this.el, 'animated'); + }, 200); } dispose(): void { + } } diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 053f144cf51..8385bc55cab 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -15,7 +15,7 @@ import { range, firstIndex } from 'vs/base/common/arrays'; import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; -export interface IOptions { +export interface ISplitViewOptions { orientation?: Orientation; // default Orientation.VERTICAL } @@ -71,13 +71,12 @@ export class SplitView implements IDisposable { private viewItems: IViewItem[] = []; private sashItems: ISashItem[] = []; private sashDragState: ISashDragState; - private animationTimeout: number; get length(): number { return this.viewItems.length; } - constructor(private container: HTMLElement, options?: IOptions) { + constructor(private container: HTMLElement, options?: ISplitViewOptions) { options = options || {}; this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; @@ -179,25 +178,9 @@ export class SplitView implements IDisposable { private onViewChange(item: IViewItem): void { item.size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - this.setupAnimation(); this.relayout(); } - // TODO@Joao: move this to panelview - private setupAnimation(): void { - // Setup animation - if (types.isNumber(this.animationTimeout)) { - window.clearTimeout(this.animationTimeout); - } - - dom.addClass(this.el, 'animated'); - - this.animationTimeout = window.setTimeout(() => { - this.animationTimeout = null; - dom.removeClass(this.el, 'animated'); - }, 200); - } - resizeView(index: number, size: number): void { if (index < 0 || index >= this.viewItems.length - 1) { throw new Error('Cant resize view'); From 86dace91a2ddc6cb61f06b047b00c04b44f237c4 Mon Sep 17 00:00:00 2001 From: Putta Khunchalee Date: Sun, 17 Sep 2017 21:18:54 +0700 Subject: [PATCH 20/67] replace bash shebang with '#!/usr/bin/env bash" for portability --- build/tfs/common/common.sh | 2 +- build/tfs/common/node.sh | 2 +- build/tfs/linux/build-ia32.sh | 2 +- build/tfs/linux/build-x64.sh | 2 +- build/tfs/linux/build.sh | 2 +- build/tfs/linux/ia32/run-agent.sh | 2 +- build/tfs/linux/ia32/xvfb.init | 2 +- build/tfs/linux/release.sh | 2 +- build/tfs/linux/repoapi_client.sh | 2 +- build/tfs/linux/smoketest.sh | 2 +- build/tfs/linux/x64/run-agent.sh | 2 +- build/tfs/linux/x64/xvfb.init | 2 +- resources/linux/debian/postrm.template | 2 +- scripts/env.sh | 2 +- scripts/npm.sh | 2 +- scripts/test-integration.sh | 2 +- scripts/test-mocha.sh | 2 +- scripts/test.sh | 2 +- src/vs/base/node/terminateProcess.sh | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/build/tfs/common/common.sh b/build/tfs/common/common.sh index 52f53537943..cdd656676a3 100755 --- a/build/tfs/common/common.sh +++ b/build/tfs/common/common.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # set agent specific npm cache diff --git a/build/tfs/common/node.sh b/build/tfs/common/node.sh index 87f95a5e1d7..67f3f59552f 100755 --- a/build/tfs/common/node.sh +++ b/build/tfs/common/node.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # setup nvm diff --git a/build/tfs/linux/build-ia32.sh b/build/tfs/linux/build-ia32.sh index 0b0f1c2a458..0d9f98c692d 100755 --- a/build/tfs/linux/build-ia32.sh +++ b/build/tfs/linux/build-ia32.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash set -e ./build/tfs/linux/build.sh ia32 "$@" \ No newline at end of file diff --git a/build/tfs/linux/build-x64.sh b/build/tfs/linux/build-x64.sh index fb5b38e02b3..e193a01a5b7 100755 --- a/build/tfs/linux/build-x64.sh +++ b/build/tfs/linux/build-x64.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash set -e ./build/tfs/linux/build.sh x64 "$@" \ No newline at end of file diff --git a/build/tfs/linux/build.sh b/build/tfs/linux/build.sh index b3d1825c2d9..4af53947033 100755 --- a/build/tfs/linux/build.sh +++ b/build/tfs/linux/build.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash . ./build/tfs/common/node.sh . ./scripts/env.sh diff --git a/build/tfs/linux/ia32/run-agent.sh b/build/tfs/linux/ia32/run-agent.sh index bcf9017f3cf..efcc96632a3 100755 --- a/build/tfs/linux/ia32/run-agent.sh +++ b/build/tfs/linux/ia32/run-agent.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ ! -f pat ]; then echo "Error: file pat not found" diff --git a/build/tfs/linux/ia32/xvfb.init b/build/tfs/linux/ia32/xvfb.init index 4d77d253a26..74f6e3b2522 100644 --- a/build/tfs/linux/ia32/xvfb.init +++ b/build/tfs/linux/ia32/xvfb.init @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # /etc/rc.d/init.d/xvfbd # diff --git a/build/tfs/linux/release.sh b/build/tfs/linux/release.sh index 41f6d35e675..e4d2009a172 100755 --- a/build/tfs/linux/release.sh +++ b/build/tfs/linux/release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash . ./scripts/env.sh . ./build/tfs/common/common.sh diff --git a/build/tfs/linux/repoapi_client.sh b/build/tfs/linux/repoapi_client.sh index b214ef10726..80de4db4a2a 100755 --- a/build/tfs/linux/repoapi_client.sh +++ b/build/tfs/linux/repoapi_client.sh @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/usr/bin/env bash -e # This is a VERY basic script for Create/Delete operations on repos and packages # cmd=$1 diff --git a/build/tfs/linux/smoketest.sh b/build/tfs/linux/smoketest.sh index 308fb047590..9866874a208 100644 --- a/build/tfs/linux/smoketest.sh +++ b/build/tfs/linux/smoketest.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e . ./build/tfs/common/node.sh diff --git a/build/tfs/linux/x64/run-agent.sh b/build/tfs/linux/x64/run-agent.sh index 76978ce2b02..1f122aa40dd 100755 --- a/build/tfs/linux/x64/run-agent.sh +++ b/build/tfs/linux/x64/run-agent.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ ! -f pat ]; then echo "Error: file pat not found" diff --git a/build/tfs/linux/x64/xvfb.init b/build/tfs/linux/x64/xvfb.init index 4d77d253a26..74f6e3b2522 100644 --- a/build/tfs/linux/x64/xvfb.init +++ b/build/tfs/linux/x64/xvfb.init @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # /etc/rc.d/init.d/xvfbd # diff --git a/resources/linux/debian/postrm.template b/resources/linux/debian/postrm.template index c43a2b16ae3..1dfa892a0ea 100755 --- a/resources/linux/debian/postrm.template +++ b/resources/linux/debian/postrm.template @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. diff --git a/scripts/env.sh b/scripts/env.sh index 35d09f66bb2..f530bf28369 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash export npm_config_disturl=https://atom.io/download/electron export npm_config_target=$(node -p "require('./package.json').electronVersion") export npm_config_runtime=electron diff --git a/scripts/npm.sh b/scripts/npm.sh index 69c6d0c48ae..02268eafd6e 100755 --- a/scripts/npm.sh +++ b/scripts/npm.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index d0154a4101d..2bfd21a20c7 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/scripts/test-mocha.sh b/scripts/test-mocha.sh index 9aa16fa3241..5d1d71a2da2 100755 --- a/scripts/test-mocha.sh +++ b/scripts/test-mocha.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } diff --git a/scripts/test.sh b/scripts/test.sh index ce1e5e11856..157c6da2cc7 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/src/vs/base/node/terminateProcess.sh b/src/vs/base/node/terminateProcess.sh index acdcbf8ed42..dceeae9745f 100755 --- a/src/vs/base/node/terminateProcess.sh +++ b/src/vs/base/node/terminateProcess.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash terminateTree() { for cpid in $(/usr/bin/pgrep -P $1); do From 18715a07e379d18310d6464cda0ee80f58cff446 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 16:23:47 +0200 Subject: [PATCH 21/67] splitview: moveView --- .../base/browser/ui/splitview/splitview2.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 8385bc55cab..f7567f763a9 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -146,6 +146,24 @@ export class SplitView implements IDisposable { this.relayoutPreferredSizes(); } + moveView(from: number, to: number): void { + if (from < 0 || from >= this.viewItems.length) { + return; + } + + if (to < 0 || to >= this.viewItems.length) { + return; + } + + if (from === to) { + return; + } + + const viewItem = this.viewItems.splice(from, 1)[0]; + this.viewItems.splice(to, 0, viewItem); + this.render(); + } + private relayoutPreferredSizes(): void { this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); this.relayout(); @@ -220,11 +238,15 @@ export class SplitView implements IDisposable { deltaDown += viewDelta; item.size = size; } - - this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); - this.sashItems.forEach(item => item.sash.layout()); } + this.render(); + } + + private render(): void { + this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); + this.sashItems.forEach(item => item.sash.layout()); + // Update sashes enablement let previous = false; const collapsesDown = this.viewItems.map(i => previous = (i.size - i.view.minimumSize > 0) || previous); From d522fb67cf7e3a3b28a9b657a46db738c260864e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 16:59:11 +0200 Subject: [PATCH 22/67] panelview: dnd --- src/vs/base/browser/ui/splitview/panelview.ts | 120 +++++++++++++++++- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 157be8261dc..97c8e20274c 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -6,13 +6,14 @@ 'use strict'; import 'vs/css!./splitview'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import Event, { Emitter, chain } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; import { firstIndex } from 'vs/base/common/arrays'; +import { Color } from 'vs/base/common/color'; import { SplitView, IView } from './splitview2'; enum PanelState { @@ -36,7 +37,7 @@ export abstract class Panel implements IView { private _minimumBodySize: number; private _maximumBodySize: number; private ariaHeaderLabel: string; - private header: HTMLElement; + readonly header: HTMLElement; private disposables: IDisposable[] = []; get minimumBodySize(): number { @@ -155,6 +156,95 @@ export abstract class Panel implements IView { } } +interface IDndContext { + draggable: PanelDraggable | null; +} + +class PanelDraggable implements IDisposable { + + // see https://github.com/Microsoft/vscode/issues/14470 + private dragOverCounter = 0; + private dropBackground: Color | undefined; + private disposables: IDisposable[] = []; + + private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); + readonly onDidDrop = this._onDidDrop.event; + + constructor(private panel: Panel, private context: IDndContext) { + domEvent(panel.header, 'dragstart')(this.onDragStart, this, this.disposables); + domEvent(panel.header, 'dragenter')(this.onDragEnter, this, this.disposables); + domEvent(panel.header, 'dragleave')(this.onDragLeave, this, this.disposables); + domEvent(panel.header, 'dragend')(this.onDragEnd, this, this.disposables); + domEvent(panel.header, 'drop')(this.onDrop, this, this.disposables); + } + + private onDragStart(e: DragEvent): void { + e.dataTransfer.effectAllowed = 'move'; + + const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.header.textContent)); + e.dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => document.body.removeChild(dragImage), 0); + + this.context.draggable = this; + } + + private onDragEnter(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter++; + this.renderHeader(); + } + + private onDragLeave(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter--; + + if (this.dragOverCounter === 0) { + this.renderHeader(); + } + } + + private onDragEnd(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + this.context.draggable = null; + } + + private onDrop(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + + if (this.context.draggable !== this) { + this._onDidDrop.fire({ from: this.context.draggable.panel, to: this.panel }); + } + + this.context.draggable = null; + } + + private renderHeader(): void { + this.panel.header.style.backgroundColor = this.dragOverCounter === 0 && this.dropBackground + ? this.dropBackground.toString() + : null; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + export class IPanelViewOptions { dnd?: boolean; } @@ -166,19 +256,30 @@ interface IPanelItem { export class PanelView implements IDisposable { + private dnd: boolean; + private dndContext: IDndContext = { draggable: null }; private el: HTMLElement; private panelItems: IPanelItem[] = []; private splitview: SplitView; private animationTimer: number | null = null; constructor(private container: HTMLElement, options?: IPanelViewOptions) { + this.dnd = !!options.dnd; this.el = append(container, $('.monaco-panel-view')); this.splitview = new SplitView(container); } addPanel(panel: Panel, size: number, index = this.splitview.length): void { - const disposable = panel.onDidChange(this.setupAnimation, this); - const panelItem = { panel, disposable }; + const disposables: IDisposable[] = []; + panel.onDidChange(this.setupAnimation, this, disposables); + + if (this.dnd) { + const draggable = new PanelDraggable(panel, this.dndContext); + disposables.push(draggable); + draggable.onDidDrop(({ from, to }) => this.movePanel(from, to), null, disposables); + } + + const panelItem = { panel, disposable: combinedDisposable(disposables) }; this.panelItems.splice(index, 0, panelItem); this.splitview.addView(panel, size, index); @@ -196,6 +297,17 @@ export class PanelView implements IDisposable { panelItem.disposable.dispose(); } + movePanel(from: Panel, to: Panel): void { + const fromIndex = firstIndex(this.panelItems, item => item.panel === from); + const toIndex = firstIndex(this.panelItems, item => item.panel === to); + + if (fromIndex === -1 || toIndex === -1) { + return; + } + + this.splitview.moveView(fromIndex, toIndex); + } + layout(size: number): void { this.splitview.layout(size); } From 67a7154be69857cb5010cdcae150c81800df1958 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 17:05:44 +0200 Subject: [PATCH 23/67] panelview: dnd background color --- src/vs/base/browser/ui/splitview/panelview.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 97c8e20274c..2715c1cc0b3 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -13,7 +13,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; import { firstIndex } from 'vs/base/common/arrays'; -import { Color } from 'vs/base/common/color'; +import { Color, RGBA } from 'vs/base/common/color'; import { SplitView, IView } from './splitview2'; enum PanelState { @@ -28,6 +28,10 @@ export interface IPanelOptions { collapsed?: boolean; } +export interface IPanelStyles { + dropBackground?: Color; +} + export abstract class Panel implements IView { private static HEADER_SIZE = 22; @@ -157,14 +161,16 @@ export abstract class Panel implements IView { } interface IDndContext { + dropBackground: Color | undefined; draggable: PanelDraggable | null; } class PanelDraggable implements IDisposable { + private static DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5)); + // see https://github.com/Microsoft/vscode/issues/14470 private dragOverCounter = 0; - private dropBackground: Color | undefined; private disposables: IDisposable[] = []; private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); @@ -235,9 +241,13 @@ class PanelDraggable implements IDisposable { } private renderHeader(): void { - this.panel.header.style.backgroundColor = this.dragOverCounter === 0 && this.dropBackground - ? this.dropBackground.toString() - : null; + let backgroundColor: string = null; + + if (this.dragOverCounter > 0) { + backgroundColor = (this.context.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString(); + } + + this.panel.header.style.backgroundColor = backgroundColor; } dispose(): void { @@ -257,7 +267,7 @@ interface IPanelItem { export class PanelView implements IDisposable { private dnd: boolean; - private dndContext: IDndContext = { draggable: null }; + private dndContext: IDndContext = { dropBackground: undefined, draggable: null }; private el: HTMLElement; private panelItems: IPanelItem[] = []; private splitview: SplitView; @@ -312,7 +322,10 @@ export class PanelView implements IDisposable { this.splitview.layout(size); } - // TODO@Joao: move this to panelview + style(styles: IPanelStyles): void { + this.dndContext.dropBackground = styles.dropBackground; + } + private setupAnimation(): void { if (typeof this.animationTimer === 'number') { window.clearTimeout(this.animationTimer); From 601f2c904899b95a091fbb63a1fffbbff692faae Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 21:34:25 +0200 Subject: [PATCH 24/67] splitview: :lipstick: --- src/vs/base/browser/ui/splitview/splitview2.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index f7567f763a9..dceae97209f 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -76,8 +76,7 @@ export class SplitView implements IDisposable { return this.viewItems.length; } - constructor(private container: HTMLElement, options?: ISplitViewOptions) { - options = options || {}; + constructor(private container: HTMLElement, options: ISplitViewOptions = {}) { this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; this.el = document.createElement('div'); @@ -161,7 +160,7 @@ export class SplitView implements IDisposable { const viewItem = this.viewItems.splice(from, 1)[0]; this.viewItems.splice(to, 0, viewItem); - this.render(); + this.layoutViews(); } private relayoutPreferredSizes(): void { @@ -240,10 +239,10 @@ export class SplitView implements IDisposable { } } - this.render(); + this.layoutViews(); } - private render(): void { + private layoutViews(): void { this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); this.sashItems.forEach(item => item.sash.layout()); From 3233f1aae075c2c1f8fa61c3a7882753f5441ca0 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 10:45:46 +0200 Subject: [PATCH 25/67] panelview: hide/show headers --- src/vs/base/browser/ui/splitview/panelview.ts | 105 ++++++++++-------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 2715c1cc0b3..7bd8c7af6aa 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -16,16 +16,11 @@ import { firstIndex } from 'vs/base/common/arrays'; import { Color, RGBA } from 'vs/base/common/color'; import { SplitView, IView } from './splitview2'; -enum PanelState { - Expanded, - Collapsed -} - export interface IPanelOptions { ariaHeaderLabel?: string; minimumBodySize?: number; maximumBodySize?: number; - collapsed?: boolean; + expanded?: boolean; } export interface IPanelStyles { @@ -36,7 +31,8 @@ export abstract class Panel implements IView { private static HEADER_SIZE = 22; - private state: PanelState = PanelState.Expanded; + private _expanded: boolean; + private _headerVisible: boolean; private _onDidChange = new Emitter(); private _minimumBodySize: number; private _maximumBodySize: number; @@ -63,20 +59,56 @@ export abstract class Panel implements IView { } get minimumSize(): number { - return Panel.HEADER_SIZE + (this.state === PanelState.Collapsed ? 0 : this._minimumBodySize); + const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + const expanded = !this.headerVisible || this.expanded; + const minimumBodySize = expanded ? this._minimumBodySize : 0; + + return headerSize + minimumBodySize; } get maximumSize(): number { - return Panel.HEADER_SIZE + (this.state === PanelState.Collapsed ? 0 : this._maximumBodySize); + const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + const expanded = !this.headerVisible || this.expanded; + const maximumBodySize = expanded ? this._maximumBodySize : 0; + + return headerSize + maximumBodySize; } readonly onDidChange: Event = this._onDidChange.event; constructor(options: IPanelOptions = {}) { + this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; this.ariaHeaderLabel = options.ariaHeaderLabel || ''; this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; - this.state = options.collapsed ? PanelState.Collapsed : PanelState.Expanded; + } + + get expanded(): boolean { + return this._expanded; + } + + set expanded(expanded: boolean) { + if (this._expanded === !!expanded) { + return; + } + + this._expanded = !!expanded; + this.renderHeader(); + this._onDidChange.fire(); + } + + get headerVisible(): boolean { + return this._headerVisible; + } + + set headerVisible(visible: boolean) { + if (this._headerVisible === !!visible) { + return; + } + + this._headerVisible = !!visible; + this.renderHeader(); + this._onDidChange.fire(); } render(container: HTMLElement): void { @@ -92,15 +124,16 @@ export abstract class Panel implements IView { .map(e => new StandardKeyboardEvent(e)); onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) - .event(this.toggleExpansion, this, this.disposables); + .event(() => this.expanded = !this.expanded, null, this.disposables); onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) - .event(this.collapse, this, this.disposables); + .event(() => this.expanded = false, null, this.disposables); onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) - .event(this.expand, this, this.disposables); + .event(() => this.expanded = true, null, this.disposables); - domEvent(header, 'click')(this.toggleExpansion, this, this.disposables); + domEvent(header, 'click') + (() => this.expanded = !this.expanded, null, this.disposables); // TODO@Joao move this down to panelview // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow) @@ -114,42 +147,20 @@ export abstract class Panel implements IView { } layout(size: number): void { - this.layoutBody(size - Panel.HEADER_SIZE); + const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + this.layoutBody(size - headerSize); } focus(): void { // TODO@joao what to do } - toggleExpansion(): void { - if (this.state === PanelState.Expanded) { - return this.collapse(); - } else { - return this.expand(); - } - } - - expand(): void { - if (this.state === PanelState.Expanded) { - return; - } - - this.renderHeader(); - this._onDidChange.fire(); - } - - collapse(): void { - if (this.state === PanelState.Collapsed) { - return; - } - - this.renderHeader(); - this._onDidChange.fire(); - } - private renderHeader(): void { - toggleClass(this.header, 'expanded', this.state === PanelState.Expanded); - this.header.setAttribute('aria-expanded', String(this.state === PanelState.Expanded)); + const expanded = !this.headerVisible || this.expanded; + + toggleClass(this.header, 'hidden', !this.headerVisible); + toggleClass(this.header, 'expanded', expanded); + this.header.setAttribute('aria-expanded', String(expanded)); } protected abstract renderBody(container: HTMLElement): void; @@ -273,6 +284,9 @@ export class PanelView implements IDisposable { private splitview: SplitView; private animationTimer: number | null = null; + private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); + readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event; + constructor(private container: HTMLElement, options?: IPanelViewOptions) { this.dnd = !!options.dnd; this.el = append(container, $('.monaco-panel-view')); @@ -286,7 +300,7 @@ export class PanelView implements IDisposable { if (this.dnd) { const draggable = new PanelDraggable(panel, this.dndContext); disposables.push(draggable); - draggable.onDidDrop(({ from, to }) => this.movePanel(from, to), null, disposables); + draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop, disposables); } const panelItem = { panel, disposable: combinedDisposable(disposables) }; @@ -340,6 +354,7 @@ export class PanelView implements IDisposable { } dispose(): void { - + this.panelItems.forEach(i => i.disposable.dispose()); + this.splitview.dispose(); } } From 07cd08c7e522899de4a27e3a4f18a9e5e2fa38d9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 11:28:49 +0200 Subject: [PATCH 26/67] introduce PanelViewlet --- src/vs/base/browser/ui/splitview/panelview.ts | 3 +- .../workbench/browser/parts/views/views2.ts | 238 ++++++++++++++++++ 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/browser/parts/views/views2.ts diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 7bd8c7af6aa..94ed8a2e53e 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -37,8 +37,9 @@ export abstract class Panel implements IView { private _minimumBodySize: number; private _maximumBodySize: number; private ariaHeaderLabel: string; + readonly header: HTMLElement; - private disposables: IDisposable[] = []; + protected disposables: IDisposable[] = []; get minimumBodySize(): number { return this._minimumBodySize; diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts new file mode 100644 index 00000000000..e6592e5a305 --- /dev/null +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import Event, { Emitter } from 'vs/base/common/event'; +import { attachStyler } from 'vs/platform/theme/common/styler'; +import { Dimension, Builder } from 'vs/base/browser/builder'; +import { append, $ } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { firstIndex } from 'vs/base/common/arrays'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { prepareActions } from 'vs/workbench/browser/actions'; +import { Viewlet, ViewletRegistry, Extensions } from 'vs/workbench/browser/viewlet'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { PanelView, IPanelOptions, Panel } from 'vs/base/browser/ui/splitview/panelview'; + +export interface IViewletPanelOptions extends IPanelOptions { + actionRunner?: IActionRunner; +} + +export abstract class ViewletPanel extends Panel { + + private _onDidFocus = new Emitter(); + readonly onDidFocus: Event = this._onDidFocus.event; + + private actionRunner: IActionRunner; + private toolbar: ToolBar; + + constructor( + readonly name: string, + initialSize: number, + options: IViewletPanelOptions, + protected keybindingService: IKeybindingService, + protected contextMenuService: IContextMenuService + ) { + super(options); + + this.actionRunner = options.actionRunner; + } + + render(container: HTMLElement): void { + super.render(container); + + const title = append(this.header, $('div.title')); + title.textContent = this.name; + + const actions = append(this.header, $('div.actions')); + this.toolbar = new ToolBar(actions, this.contextMenuService, { + orientation: ActionsOrientation.HORIZONTAL, + actionItemProvider: action => this.getActionItem(action), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.name), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionRunner: this.actionRunner + }); + + this.disposables.push(this.toolbar); + this.updateActions(); + } + + focus(): void { + super.focus(); + this._onDidFocus.fire(); + } + + protected updateActions(): void { + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions()))(); + this.toolbar.context = this.getActionsContext(); + } + + getActions(): IAction[] { + return []; + } + + getSecondaryActions(): IAction[] { + return []; + } + + getActionItem(action: IAction): IActionItem { + return null; + } + + getActionsContext(): any { + return undefined; + } + + getOptimalWidth(): number { + return 0; + } +} + +export interface IViewsViewletOptions { + showHeaderInTitleWhenSingleView: boolean; +} + +const SplitViewThemeMapping = { + dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND +}; + +interface IViewletPanelItem { + panel: ViewletPanel; + disposable: IDisposable; +} + +export class PanelViewlet extends Viewlet { + + protected lastFocusedPanel: ViewletPanel | undefined; + private panelItems: IViewletPanelItem[] = []; + private panelview: PanelView; + + protected get isSingleView(): boolean { + return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1; + } + + protected get length(): number { + return this.panelItems.length; + } + + constructor( + id: string, + private options: Partial, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService + ) { + super(id, telemetryService, themeService); + } + + async create(parent: Builder): TPromise { + super.create(parent); + + const container = parent.getHTMLElement(); + this.panelview = this._register(new PanelView(container)); + this._register(attachStyler(this.themeService, SplitViewThemeMapping, this.panelview)); + // this._register(this.panelview.onFocus(view => this.lastFocusedView = view)); + } + + getTitle(): string { + let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; + + if (this.isSingleView) { + title += ': ' + this.panelItems[0].panel.name; + } + + return title; + } + + getActions(): IAction[] { + if (this.isSingleView) { + return this.panelItems[0].panel.getActions(); + } + + return []; + } + + getSecondaryActions(): IAction[] { + if (this.isSingleView) { + return this.panelItems[0].panel.getSecondaryActions(); + } + + return []; + } + + focus(): void { + super.focus(); + + if (this.lastFocusedPanel) { + this.lastFocusedPanel.focus(); + } else if (this.panelItems.length > 0) { + this.panelItems[0].panel.focus(); + } + } + + layout(dimension: Dimension): void { + this.panelview.layout(dimension.height); + } + + getOptimalWidth(): number { + const sizes = this.panelItems + .map(panelItem => panelItem.panel.getOptimalWidth() || 0); + + return Math.max(...sizes); + } + + addView(panel: ViewletPanel, index = this.panelItems.length - 1): void { + const disposables: IDisposable[] = []; + const onDidFocus = panel.onDidFocus(() => this.lastFocusedPanel = panel, null, disposables); + const disposable = combinedDisposable([onDidFocus]); + const panelItem: IViewletPanelItem = { panel, disposable }; + + this.panelItems.splice(index, 0, panelItem); + this.panelview.addPanel(panel, 200, index); + + this.updateViewHeaders(); + this.updateTitleArea(); + } + + removeView(panel: ViewletPanel): void { + const index = firstIndex(this.panelItems, i => i.panel === panel); + + if (index === -1) { + return; + } + + if (this.lastFocusedPanel === panel) { + this.lastFocusedPanel = undefined; + } + + this.panelview.removePanel(panel); + const [panelItem] = this.panelItems.splice(index, 1); + panelItem.disposable.dispose(); + + this.updateViewHeaders(); + this.updateTitleArea(); + } + + private updateViewHeaders(): void { + if (this.isSingleView) { + this.panelItems[0].panel.headerVisible = false; + } else { + this.panelItems.forEach(i => i.panel.headerVisible = true); + } + } + + dispose(): void { + super.dispose(); + this.panelItems.forEach(i => i.disposable.dispose()); + this.panelview.dispose(); + } +} \ No newline at end of file From 1090fc63ebefc12e2024aa36c86472b05715c765 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 15:25:49 +0200 Subject: [PATCH 27/67] wip: more panelview work, PanelViewlet --- .../base/browser/ui/splitview/panelview.css | 67 +++++ src/vs/base/browser/ui/splitview/panelview.ts | 83 +++--- .../base/browser/ui/splitview/splitview2.css | 23 ++ .../base/browser/ui/splitview/splitview2.ts | 5 +- src/vs/platform/theme/common/styler.ts | 8 +- .../workbench/browser/parts/views/views2.ts | 61 +++-- .../parts/scm/electron-browser/scmViewlet.ts | 257 ++++++++---------- 7 files changed, 296 insertions(+), 208 deletions(-) create mode 100644 src/vs/base/browser/ui/splitview/panelview.css create mode 100644 src/vs/base/browser/ui/splitview/splitview2.css diff --git a/src/vs/base/browser/ui/splitview/panelview.css b/src/vs/base/browser/ui/splitview/panelview.css new file mode 100644 index 00000000000..b22f256de7e --- /dev/null +++ b/src/vs/base/browser/ui/splitview/panelview.css @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-panel-view { + width: 100%; + height: 100%; +} + +.monaco-panel-view .panel { + overflow: hidden; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.monaco-panel-view .panel > .panel-header { + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + padding-left: 20px; + overflow: hidden; +} + +/* Bold font style does not go well with CJK fonts */ +.monaco-panel-view:lang(zh-Hans) .panel > .panel-header, +.monaco-panel-view:lang(zh-Hant) .panel > .panel-header, +.monaco-panel-view:lang(ja) .panel > .panel-header, +.monaco-panel-view:lang(ko) .panel > .panel-header { + font-weight: normal; +} + +.monaco-panel-view .panel > .panel-header.hidden { + display: none; +} + +.monaco-panel-view .panel > .panel-body { + overflow: hidden; + flex: 1; +} + +.monaco-panel-view .panel > .panel-header { + cursor: pointer; + /* background: rgba(128, 128, 128, 0.2); */ +} + +.monaco-panel-view .panel > .panel-header { + background-image: url('arrow-collapse.svg'); + background-position: 2px center; + background-repeat: no-repeat; +} + +.monaco-panel-view .panel > .panel-header.expanded { + background-image: url('arrow-expand.svg'); + background-position: 2px center; + background-repeat: no-repeat; +} + +.vs-dark .monaco-panel-view .panel > .panel-header { + background-image: url('arrow-collapse-dark.svg'); +} + +.vs-dark .monaco-panel-view .panel > .panel-header.expanded { + background-image: url('arrow-expand-dark.svg'); +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 94ed8a2e53e..e676d0de5f6 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -5,7 +5,7 @@ 'use strict'; -import 'vs/css!./splitview'; +import 'vs/css!./panelview'; import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import Event, { Emitter, chain } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; @@ -25,6 +25,9 @@ export interface IPanelOptions { export interface IPanelStyles { dropBackground?: Color; + headerForeground?: Color; + headerBackground?: Color; + headerHighContrastBorder?: Color; } export abstract class Panel implements IView { @@ -38,9 +41,18 @@ export abstract class Panel implements IView { private _maximumBodySize: number; private ariaHeaderLabel: string; - readonly header: HTMLElement; + private header: HTMLElement; protected disposables: IDisposable[] = []; + get draggable(): HTMLElement { + return this.header; + } + + private _dropBackground: Color | undefined; + get dropBackground(): Color | undefined { + return this._dropBackground; + } + get minimumBodySize(): number { return this._minimumBodySize; } @@ -59,8 +71,12 @@ export abstract class Panel implements IView { this._onDidChange.fire(); } + private get headerSize(): number { + return this.headerVisible ? Panel.HEADER_SIZE : 0; + } + get minimumSize(): number { - const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + const headerSize = this.headerSize; const expanded = !this.headerVisible || this.expanded; const minimumBodySize = expanded ? this._minimumBodySize : 0; @@ -68,7 +84,7 @@ export abstract class Panel implements IView { } get maximumSize(): number { - const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + const headerSize = this.headerSize; const expanded = !this.headerVisible || this.expanded; const maximumBodySize = expanded ? this._maximumBodySize : 0; @@ -82,6 +98,7 @@ export abstract class Panel implements IView { this.ariaHeaderLabel = options.ariaHeaderLabel || ''; this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; + this.header = $('.panel-header'); } get expanded(): boolean { @@ -94,7 +111,7 @@ export abstract class Panel implements IView { } this._expanded = !!expanded; - this.renderHeader(); + this.updateHeader(); this._onDidChange.fire(); } @@ -108,20 +125,21 @@ export abstract class Panel implements IView { } this._headerVisible = !!visible; - this.renderHeader(); + this.updateHeader(); this._onDidChange.fire(); } render(container: HTMLElement): void { const panel = append(container, $('.panel')); - const header = append(panel, $('.panel-header')); - header.setAttribute('tabindex', '0'); - header.setAttribute('role', 'toolbar'); - header.setAttribute('aria-label', this.ariaHeaderLabel); - this.renderHeader(); + append(panel, this.header); + this.header.setAttribute('tabindex', '0'); + this.header.setAttribute('role', 'toolbar'); + this.header.setAttribute('aria-label', this.ariaHeaderLabel); + this.renderHeader(this.header); + this.updateHeader(); - const onHeaderKeyDown = chain(domEvent(header, 'keydown')) + const onHeaderKeyDown = chain(domEvent(this.header, 'keydown')) .map(e => new StandardKeyboardEvent(e)); onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) @@ -133,7 +151,7 @@ export abstract class Panel implements IView { onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) .event(() => this.expanded = true, null, this.disposables); - domEvent(header, 'click') + domEvent(this.header, 'click') (() => this.expanded = !this.expanded, null, this.disposables); // TODO@Joao move this down to panelview @@ -152,18 +170,24 @@ export abstract class Panel implements IView { this.layoutBody(size - headerSize); } - focus(): void { - // TODO@joao what to do + style(styles: IPanelStyles): void { + this.header.style.color = styles.headerForeground ? styles.headerForeground.toString() : null; + this.header.style.backgroundColor = styles.headerBackground ? styles.headerBackground.toString() : null; + this.header.style.borderTop = styles.headerHighContrastBorder ? `1px solid ${styles.headerHighContrastBorder}` : null; + this._dropBackground = styles.dropBackground; } - private renderHeader(): void { + private updateHeader(): void { const expanded = !this.headerVisible || this.expanded; + this.header.style.height = `${this.headerSize}px`; + this.header.style.lineHeight = `${this.headerSize}px`; toggleClass(this.header, 'hidden', !this.headerVisible); toggleClass(this.header, 'expanded', expanded); this.header.setAttribute('aria-expanded', String(expanded)); } + protected abstract renderHeader(container: HTMLElement): void; protected abstract renderBody(container: HTMLElement): void; protected abstract layoutBody(size: number): void; @@ -173,7 +197,6 @@ export abstract class Panel implements IView { } interface IDndContext { - dropBackground: Color | undefined; draggable: PanelDraggable | null; } @@ -189,17 +212,17 @@ class PanelDraggable implements IDisposable { readonly onDidDrop = this._onDidDrop.event; constructor(private panel: Panel, private context: IDndContext) { - domEvent(panel.header, 'dragstart')(this.onDragStart, this, this.disposables); - domEvent(panel.header, 'dragenter')(this.onDragEnter, this, this.disposables); - domEvent(panel.header, 'dragleave')(this.onDragLeave, this, this.disposables); - domEvent(panel.header, 'dragend')(this.onDragEnd, this, this.disposables); - domEvent(panel.header, 'drop')(this.onDrop, this, this.disposables); + domEvent(panel.draggable, 'dragstart')(this.onDragStart, this, this.disposables); + domEvent(panel.draggable, 'dragenter')(this.onDragEnter, this, this.disposables); + domEvent(panel.draggable, 'dragleave')(this.onDragLeave, this, this.disposables); + domEvent(panel.draggable, 'dragend')(this.onDragEnd, this, this.disposables); + domEvent(panel.draggable, 'drop')(this.onDrop, this, this.disposables); } private onDragStart(e: DragEvent): void { e.dataTransfer.effectAllowed = 'move'; - const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.header.textContent)); + const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggable.textContent)); e.dataTransfer.setDragImage(dragImage, -10, -10); setTimeout(() => document.body.removeChild(dragImage), 0); @@ -256,10 +279,10 @@ class PanelDraggable implements IDisposable { let backgroundColor: string = null; if (this.dragOverCounter > 0) { - backgroundColor = (this.context.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString(); + backgroundColor = (this.panel.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString(); } - this.panel.header.style.backgroundColor = backgroundColor; + this.panel.draggable.style.backgroundColor = backgroundColor; } dispose(): void { @@ -279,7 +302,7 @@ interface IPanelItem { export class PanelView implements IDisposable { private dnd: boolean; - private dndContext: IDndContext = { dropBackground: undefined, draggable: null }; + private dndContext: IDndContext = { draggable: null }; private el: HTMLElement; private panelItems: IPanelItem[] = []; private splitview: SplitView; @@ -288,10 +311,10 @@ export class PanelView implements IDisposable { private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event; - constructor(private container: HTMLElement, options?: IPanelViewOptions) { + constructor(private container: HTMLElement, options: IPanelViewOptions = {}) { this.dnd = !!options.dnd; this.el = append(container, $('.monaco-panel-view')); - this.splitview = new SplitView(container); + this.splitview = new SplitView(this.el); } addPanel(panel: Panel, size: number, index = this.splitview.length): void { @@ -337,10 +360,6 @@ export class PanelView implements IDisposable { this.splitview.layout(size); } - style(styles: IPanelStyles): void { - this.dndContext.dropBackground = styles.dropBackground; - } - private setupAnimation(): void { if (typeof this.animationTimer === 'number') { window.clearTimeout(this.animationTimer); diff --git a/src/vs/base/browser/ui/splitview/splitview2.css b/src/vs/base/browser/ui/splitview/splitview2.css new file mode 100644 index 00000000000..41d3de5c826 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/splitview2.css @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-split-view2 { + position: relative; + overflow: hidden; + width: 100%; + height: 100%; +} + +.monaco-split-view2 > .split-view-view { + overflow: hidden; +} + +.monaco-split-view2.vertical > .split-view-view { + width: 100%; +} + +.monaco-split-view2.horizontal > .split-view-view { + height: 100%; +} diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index dceae97209f..92a28e0cb26 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -5,7 +5,7 @@ 'use strict'; -import 'vs/css!./splitview'; +import 'vs/css!./splitview2'; import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; import types = require('vs/base/common/types'); @@ -25,7 +25,6 @@ export interface IView { readonly onDidChange: Event; render(container: HTMLElement, orientation: Orientation): void; layout(size: number, orientation: Orientation): void; - focus(): void; } interface ISashEvent { @@ -80,7 +79,7 @@ export class SplitView implements IDisposable { this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; this.el = document.createElement('div'); - dom.addClass(this.el, 'monaco-split-view'); + dom.addClass(this.el, 'monaco-split-view2'); dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal'); container.appendChild(this.el); } diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 243d6c820ac..d5d4698e424 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -15,11 +15,15 @@ export interface IThemable { style: styleFn; } -export function attachStyler(themeService: IThemeService, optionsMapping: { [optionsKey: string]: ColorIdentifier | ColorFunction }, widgetOrCallback: IThemable | styleFn): IDisposable { +export interface IColorMapping { + [optionsKey: string]: ColorIdentifier | ColorFunction | undefined; +} + +export function attachStyler(themeService: IThemeService, optionsMapping: T, widgetOrCallback: IThemable | styleFn): IDisposable { function applyStyles(theme: ITheme): void { const styles = Object.create(null); for (let key in optionsMapping) { - const value = optionsMapping[key]; + const value = optionsMapping[key as string]; if (typeof value === 'string') { styles[key] = theme.getColor(value); } else if (typeof value === 'function') { diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts index e6592e5a305..cd3bc72e93c 100644 --- a/src/vs/workbench/browser/parts/views/views2.ts +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -6,7 +6,9 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; -import { attachStyler } from 'vs/platform/theme/common/styler'; +import { ColorIdentifier, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler, IColorMapping, IThemable } from 'vs/platform/theme/common/styler'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND } from 'vs/workbench/common/theme'; import { Dimension, Builder } from 'vs/base/browser/builder'; import { append, $ } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; @@ -21,9 +23,24 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { PanelView, IPanelOptions, Panel } from 'vs/base/browser/ui/splitview/panelview'; +export interface IPanelColors extends IColorMapping { + dropBackground?: ColorIdentifier; + headerForeground?: ColorIdentifier; + headerBackground?: ColorIdentifier; + headerHighContrastBorder?: ColorIdentifier; +} + +export function attachPanelStyler(widget: IThemable, themeService: IThemeService) { + return attachStyler(themeService, { + headerForeground: SIDE_BAR_SECTION_HEADER_FOREGROUND, + headerBackground: SIDE_BAR_SECTION_HEADER_BACKGROUND, + headerHighContrastBorder: contrastBorder, + dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND + }, widget); +} + export interface IViewletPanelOptions extends IPanelOptions { actionRunner?: IActionRunner; } @@ -37,28 +54,24 @@ export abstract class ViewletPanel extends Panel { private toolbar: ToolBar; constructor( - readonly name: string, - initialSize: number, + readonly title: string, options: IViewletPanelOptions, - protected keybindingService: IKeybindingService, - protected contextMenuService: IContextMenuService + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService ) { super(options); this.actionRunner = options.actionRunner; } - render(container: HTMLElement): void { - super.render(container); + protected renderHeader(container: HTMLElement): void { + this.renderHeaderTitle(container); - const title = append(this.header, $('div.title')); - title.textContent = this.name; - - const actions = append(this.header, $('div.actions')); + const actions = append(container, $('.actions')); this.toolbar = new ToolBar(actions, this.contextMenuService, { orientation: ActionsOrientation.HORIZONTAL, actionItemProvider: action => this.getActionItem(action), - ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.name), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), actionRunner: this.actionRunner }); @@ -67,8 +80,11 @@ export abstract class ViewletPanel extends Panel { this.updateActions(); } + protected renderHeaderTitle(container: HTMLElement): void { + append(container, $('.title', null, this.title)); + } + focus(): void { - super.focus(); this._onDidFocus.fire(); } @@ -102,10 +118,6 @@ export interface IViewsViewletOptions { showHeaderInTitleWhenSingleView: boolean; } -const SplitViewThemeMapping = { - dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND -}; - interface IViewletPanelItem { panel: ViewletPanel; disposable: IDisposable; @@ -139,15 +151,13 @@ export class PanelViewlet extends Viewlet { const container = parent.getHTMLElement(); this.panelview = this._register(new PanelView(container)); - this._register(attachStyler(this.themeService, SplitViewThemeMapping, this.panelview)); - // this._register(this.panelview.onFocus(view => this.lastFocusedView = view)); } getTitle(): string { let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; if (this.isSingleView) { - title += ': ' + this.panelItems[0].panel.name; + title += ': ' + this.panelItems[0].panel.title; } return title; @@ -190,20 +200,21 @@ export class PanelViewlet extends Viewlet { return Math.max(...sizes); } - addView(panel: ViewletPanel, index = this.panelItems.length - 1): void { + addPanel(panel: ViewletPanel, size: number, index = this.panelItems.length - 1): void { const disposables: IDisposable[] = []; const onDidFocus = panel.onDidFocus(() => this.lastFocusedPanel = panel, null, disposables); - const disposable = combinedDisposable([onDidFocus]); + const styler = attachPanelStyler(panel, this.themeService); + const disposable = combinedDisposable([onDidFocus, styler]); const panelItem: IViewletPanelItem = { panel, disposable }; this.panelItems.splice(index, 0, panelItem); - this.panelview.addPanel(panel, 200, index); + this.panelview.addPanel(panel, size, index); this.updateViewHeaders(); this.updateTitleArea(); } - removeView(panel: ViewletPanel): void { + removePanel(panel: ViewletPanel): void { const index = firstIndex(this.panelItems, i => i.panel === panel); if (index === -1) { diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 3835e5e3cf5..3e00a3b53e6 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -13,7 +13,7 @@ import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Builder } from 'vs/base/browser/builder'; -import { PersistentViewsViewlet, CollapsibleView, IViewletViewOptions, IViewletView, IViewOptions } from 'vs/workbench/browser/parts/views/views'; +import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/views2'; import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; @@ -43,9 +43,7 @@ import Severity from 'vs/base/common/severity'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ViewLocation, ViewsRegistry, IViewDescriptor } from 'vs/workbench/browser/parts/views/viewsRegistry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ViewSizing } from 'vs/base/browser/ui/splitview/splitview'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import * as platform from 'vs/base/common/platform'; @@ -86,13 +84,6 @@ interface IViewModel { toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); } -class ProvidersViewDescriptor implements IViewDescriptor { - readonly id = 'providers'; - readonly name = ''; - readonly location = ViewLocation.SCM; - readonly ctor = null; -} - class ProvidersListDelegate implements IDelegate { getHeight(element: ISCMRepository): number { @@ -211,31 +202,19 @@ class ProviderRenderer implements IRenderer; constructor( - initialSize: number, protected viewModel: IViewModel, - options: IViewletViewOptions, @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, @ISCMService protected scmService: ISCMService, @IInstantiationService private instantiationService: IInstantiationService ) { - super(initialSize, { - ...(options as IViewOptions), - sizing: ViewSizing.Fixed, - name: localize('scm providers', "Source Control Providers"), - }, keybindingService, contextMenuService); - } - - renderHeader(container: HTMLElement): void { - const title = append(container, $('div.title')); - title.textContent = this.name; - - super.renderHeader(container); + super(localize('scm providers', "Source Control Providers"), {}, keybindingService, contextMenuService); + this.updateBodySize(); } protected renderBody(container: HTMLElement): void { @@ -243,35 +222,32 @@ class ProvidersView extends CollapsibleView { const renderer = this.instantiationService.createInstance(ProviderRenderer, this.viewModel); this.list = new List(container, delegate, [renderer]); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.toDispose); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.toDispose); - this.updateList(); + this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + this.update(); } - layoutBody(size: number): void { - if (!this.list) { - return; - } - + protected layoutBody(size: number): void { this.list.layout(size); } - private updateList(): void { - this.list.splice(0, this.list.length, this.scmService.repositories); - } - private onDidAddRepository(repository: ISCMRepository): void { - this.updateList(); - this.setBodySize(this.getExpandedBodySize()); + this.update(); } private onDidRemoveRepository(repository: ISCMRepository): void { - this.updateList(); - this.setBodySize(this.getExpandedBodySize()); + this.update(); } - private getExpandedBodySize(): number { - return Math.min(5, this.scmService.repositories.length) * 22; + private update(): void { + this.list.splice(0, this.list.length, this.scmService.repositories); + this.updateBodySize(); + } + + private updateBodySize(): void { + const size = Math.min(5, this.scmService.repositories.length) * 22; + this.minimumBodySize = size; + this.maximumBodySize = size; } } @@ -420,38 +396,38 @@ class ProviderListDelegate implements IDelegate 0) { - this.id = ProviderViewDescriptor.freeIds.shift(); - } else { - this.id = `scm${ProviderViewDescriptor.idCount++}`; - } - } +// constructor(private _repository: ISCMRepository) { +// if (ProviderViewDescriptor.freeIds.length > 0) { +// this.id = ProviderViewDescriptor.freeIds.shift(); +// } else { +// this.id = `scm${ProviderViewDescriptor.idCount++}`; +// } +// } - dispose(): void { - ProviderViewDescriptor.freeIds.push(this.id); - } -} +// dispose(): void { +// ProviderViewDescriptor.freeIds.push(this.id); +// } +// } -class ProviderView extends CollapsibleView { +class ProviderPanel extends ViewletPanel { private cachedHeight: number | undefined; private inputBoxContainer: HTMLElement; @@ -459,12 +435,9 @@ class ProviderView extends CollapsibleView { private listContainer: HTMLElement; private list: List; private menus: SCMMenus; - private disposables: IDisposable[] = []; constructor( - initialSize: number, private repository: ISCMRepository, - options: IViewletViewOptions, @IKeybindingService protected keybindingService: IKeybindingService, @IThemeService protected themeService: IThemeService, @IContextMenuService protected contextMenuService: IContextMenuService, @@ -476,13 +449,13 @@ class ProviderView extends CollapsibleView { @IEditorGroupService protected editorGroupService: IEditorGroupService, @IInstantiationService protected instantiationService: IInstantiationService ) { - super(initialSize, { ...(options as IViewOptions), sizing: ViewSizing.Flexible }, keybindingService, contextMenuService); + super(repository.provider.label, {}, keybindingService, contextMenuService); this.menus = instantiationService.createInstance(SCMMenus, repository.provider); this.menus.onDidChangeTitle(this.updateActions, this, this.disposables); } - renderHeader(container: HTMLElement): void { + protected renderHeaderTitle(container: HTMLElement): void { const header = append(container, $('.title.scm-provider')); const name = append(header, $('.name')); const title = append(name, $('span.title')); @@ -495,11 +468,9 @@ class ProviderView extends CollapsibleView { title.textContent = this.repository.provider.label; type.textContent = ''; } - - super.renderHeader(container); } - renderBody(container: HTMLElement): void { + protected renderBody(container: HTMLElement): void { const focusTracker = trackFocus(container); this.disposables.push(focusTracker.addFocusListener(() => this.repository.focus())); this.disposables.push(focusTracker); @@ -585,7 +556,9 @@ class ProviderView extends CollapsibleView { } focus(): void { - if (this.isExpanded()) { + super.focus(); + + if (this.expanded) { this.inputBox.focus(); } } @@ -697,16 +670,19 @@ class InstallAdditionalSCMProvidersAction extends Action { } } -export class SCMViewlet extends PersistentViewsViewlet { +export class SCMViewlet extends PanelViewlet { private menus: SCMMenus; - private repositoryToViewDescriptor = new Map(); + + private providersPanel: ProvidersPanel; + + // private repositoryToViewDescriptor = new Map(); private disposables: IDisposable[] = []; constructor( @ITelemetryService telemetryService: ITelemetryService, @ISCMService protected scmService: ISCMService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IContextViewService protected contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService protected keybindingService: IKeybindingService, @@ -721,67 +697,56 @@ export class SCMViewlet extends PersistentViewsViewlet { @IStorageService storageService: IStorageService, @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, ViewLocation.SCM, 'scm', false, - telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); + super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: false }, telemetryService, themeService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this.menus.onDidChangeTitle(this.updateTitleArea, this, this.disposables); } - private onDidAddRepository(repository: ISCMRepository): void { - const viewDescriptor = new ProviderViewDescriptor(repository); - this.repositoryToViewDescriptor.set(repository.provider.id, viewDescriptor); - - ViewsRegistry.registerViews([viewDescriptor]); - toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - this.updateTitleArea(); - } - - private onDidRemoveRepository(repository: ISCMRepository): void { - const viewDescriptor = this.repositoryToViewDescriptor.get(repository.provider.id); - this.repositoryToViewDescriptor.delete(repository.provider.id); - viewDescriptor.dispose(); - - ViewsRegistry.deregisterViews([viewDescriptor.id], ViewLocation.SCM); - toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - this.updateTitleArea(); - } - async create(parent: Builder): TPromise { await super.create(parent); - parent.addClass('scm-viewlet', 'empty'); - append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no source control providers active."))); + parent.addClass('scm-viewlet'/* , 'empty' */); + // append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no source control providers active."))); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.scmService.repositories.forEach(p => this.onDidAddRepository(p)); + this.providersPanel = this.instantiationService.createInstance(ProvidersPanel, this); + this.addPanel(this.providersPanel, this.providersPanel.minimumSize); - ViewsRegistry.registerViews([new ProvidersViewDescriptor()]); + // this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + // this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + // this.scmService.repositories.forEach(p => this.onDidAddRepository(p)); + + // ViewsRegistry.registerViews([new ProvidersViewDescriptor()]); } + // private onDidAddRepository(repository: ISCMRepository): void { + // const viewDescriptor = new ProviderViewDescriptor(repository); + // this.repositoryToViewDescriptor.set(repository.provider.id, viewDescriptor); + + // ViewsRegistry.registerViews([viewDescriptor]); + // toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); + // this.updateTitleArea(); + // } + + // private onDidRemoveRepository(repository: ISCMRepository): void { + // const viewDescriptor = this.repositoryToViewDescriptor.get(repository.provider.id); + // this.repositoryToViewDescriptor.delete(repository.provider.id); + // viewDescriptor.dispose(); + + // ViewsRegistry.deregisterViews([viewDescriptor.id], ViewLocation.SCM); + // toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); + // this.updateTitleArea(); + // } + isRepositoryVisible(repository: ISCMRepository): boolean { - const view = this.repositoryToViewDescriptor.get(repository.provider.id); - return !!this.getView(view.id); + // const view = this.repositoryToViewDescriptor.get(repository.provider.id); + // return !!this.getView(view.id); + return true; } toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean): void { - const view = this.repositoryToViewDescriptor.get(repository.provider.id); - this.toggleViewVisibility(view.id, visible); - } - - protected createView(viewDescriptor: IViewDescriptor, initialSize: number, options: IViewletViewOptions): IViewletView { - if (viewDescriptor instanceof ProviderViewDescriptor) { - return this.instantiationService.createInstance(ProviderView, initialSize, viewDescriptor.repository, options); - } else if (viewDescriptor instanceof ProvidersViewDescriptor) { - return this.instantiationService.createInstance(ProvidersView, initialSize, this, options); - } - - return this.instantiationService.createInstance(viewDescriptor.ctor, initialSize, options); - } - - protected getDefaultViewSize(): number | undefined { - return this.dimension && this.dimension.height / Math.max(this.views.length, 1); + // const view = this.repositoryToViewDescriptor.get(repository.provider.id); + // this.toggleViewVisibility(view.id, visible); } getOptimalWidth(): number { @@ -790,20 +755,20 @@ export class SCMViewlet extends PersistentViewsViewlet { getTitle(): string { const title = localize('source control', "Source Control"); - const views = ViewsRegistry.getViews(ViewLocation.SCM); + // const views = ViewsRegistry.getViews(ViewLocation.SCM); - if (views.length === 1) { - const view = views[0]; - return localize('viewletTitle', "{0}: {1}", title, view.name); - } else { - return title; - } + // if (views.length === 1) { + // const view = views[0]; + // return localize('viewletTitle', "{0}: {1}", title, view.name); + // } else { + return title; + // } } getActions(): IAction[] { - if (this.showHeaderInTitleArea() && this.views.length === 1) { - return this.views[0].getActions(); - } + // if (this.isSingleView) { + // return this.views[0].getActions(); + // } return this.menus.getTitleActions(); } @@ -811,18 +776,18 @@ export class SCMViewlet extends PersistentViewsViewlet { getSecondaryActions(): IAction[] { let result: IAction[]; - if (this.showHeaderInTitleArea() && this.views.length === 1) { - result = [ - ...this.views[0].getSecondaryActions(), - new Separator() - ]; - } else { - result = this.menus.getTitleSecondaryActions(); + // if (this.isSingleView) { + // result = [ + // ...this.views[0].getSecondaryActions(), + // new Separator() + // ]; + // } else { + result = this.menus.getTitleSecondaryActions(); - if (result.length > 0) { - result.push(new Separator()); - } + if (result.length > 0) { + result.push(new Separator()); } + // } result.push(this.instantiationService.createInstance(InstallAdditionalSCMProvidersAction)); From 36fff1a2aa44442f8e707e46fd55f5e4cfb482dc Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 16:13:43 +0200 Subject: [PATCH 28/67] PanelViewlet.movePanel --- src/vs/base/browser/ui/splitview/panelview.ts | 3 +++ src/vs/workbench/browser/parts/views/views2.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index e676d0de5f6..83e56ce5793 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -353,6 +353,9 @@ export class PanelView implements IDisposable { return; } + const [panelItem] = this.panelItems.splice(fromIndex, 1); + this.panelItems.splice(toIndex < fromIndex ? toIndex : toIndex - 1, 0, panelItem); + this.splitview.moveView(fromIndex, toIndex); } diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts index cd3bc72e93c..115dda093f4 100644 --- a/src/vs/workbench/browser/parts/views/views2.ts +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -233,6 +233,24 @@ export class PanelViewlet extends Viewlet { this.updateTitleArea(); } + movePanel(from: ViewletPanel, to: ViewletPanel): void { + const fromIndex = firstIndex(this.panelItems, item => item.panel === from); + const toIndex = firstIndex(this.panelItems, item => item.panel === to); + + if (fromIndex < 0 || fromIndex >= this.panelItems.length) { + return; + } + + if (toIndex < 0 || toIndex >= this.panelItems.length) { + return; + } + + + const [panelItem] = this.panelItems.splice(fromIndex, 1); + this.panelItems.splice(toIndex < fromIndex ? toIndex : toIndex - 1, 0, panelItem); + this.panelview.movePanel(from, to); + } + private updateViewHeaders(): void { if (this.isSingleView) { this.panelItems[0].panel.headerVisible = false; From a359f60ad47e62aa3a3eb3ee652bd616d05d0e66 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 16:13:54 +0200 Subject: [PATCH 29/67] wip: SCM Viewlet --- .../parts/scm/electron-browser/scmViewlet.ts | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 3e00a3b53e6..0f7d03fe781 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -17,7 +17,7 @@ import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/vie import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -68,18 +68,11 @@ class SCMMenuItemActionItem extends MenuItemActionItem { } } -function identityProvider(r: ISCMResourceGroup | ISCMResource): string { - if (isSCMResource(r)) { - const group = r.resourceGroup; - const provider = group.provider; - return `${provider.contextValue}/${group.id}/${r.sourceUri.toString()}`; - } else { - const provider = r.provider; - return `${provider.contextValue}/${r.id}`; - } -} - interface IViewModel { + addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void; + removeRepositoryPanel(panel: RepositoryPanel): void; + moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void; + isRepositoryVisible(repository: ISCMRepository): boolean; toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); } @@ -202,9 +195,11 @@ class ProviderRenderer implements IRenderer; + private repositories: ISCMRepository[] = []; + private repositoryPanels: RepositoryPanel[] = []; constructor( protected viewModel: IViewModel, @@ -220,11 +215,16 @@ class ProvidersPanel extends ViewletPanel { protected renderBody(container: HTMLElement): void { const delegate = new ProvidersListDelegate(); const renderer = this.instantiationService.createInstance(ProviderRenderer, this.viewModel); - this.list = new List(container, delegate, [renderer]); + + this.list = new List(container, delegate, [renderer], { + identityProvider: repository => repository.provider.id + }); + + this.list.onSelectionChange(this.onSelectionChange, this, this.disposables); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.update(); + this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); } protected layoutBody(size: number): void { @@ -232,20 +232,42 @@ class ProvidersPanel extends ViewletPanel { } private onDidAddRepository(repository: ISCMRepository): void { - this.update(); + this.repositories.push(repository); + this.list.splice(this.list.length, 0, [repository]); + this.updateBodySize(); + + if (this.repositories.length === 1) { + this.list.setSelection([0]); + } } private onDidRemoveRepository(repository: ISCMRepository): void { - this.update(); - } + const index = this.repositories.indexOf(repository); - private update(): void { - this.list.splice(0, this.list.length, this.scmService.repositories); + if (index === -1) { + return; + } + + this.repositories.splice(index, 1); + this.list.splice(index, 1); this.updateBodySize(); } + private onSelectionChange(e: IListEvent): void { + console.log(e.elements); + + const toRemove = this.repositoryPanels.filter(p => e.elements.every(r => p.repository !== r)); + const toAdd = e.elements.filter(r => this.repositoryPanels.every(p => p.repository !== r)); + + console.log(toAdd, toRemove); + + + // this.viewModel.addRepositoryPanel(); + // this.repositoryPanels + } + private updateBodySize(): void { - const size = Math.min(5, this.scmService.repositories.length) * 22; + const size = Math.min(5, this.repositories.length) * 22; this.minimumBodySize = size; this.maximumBodySize = size; } @@ -427,7 +449,18 @@ class ProviderListDelegate implements IDelegate(); private disposables: IDisposable[] = []; @@ -709,8 +742,8 @@ export class SCMViewlet extends PanelViewlet { parent.addClass('scm-viewlet'/* , 'empty' */); // append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no source control providers active."))); - this.providersPanel = this.instantiationService.createInstance(ProvidersPanel, this); - this.addPanel(this.providersPanel, this.providersPanel.minimumSize); + this.mainPanel = this.instantiationService.createInstance(MainPanel, this); + this.addPanel(this.mainPanel, this.mainPanel.minimumSize); // this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); // this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); @@ -802,6 +835,18 @@ export class SCMViewlet extends PanelViewlet { return new SCMMenuItemActionItem(action, this.keybindingService, this.messageService); } + addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void { + this.addPanel(panel, size, index + 1); + } + + removeRepositoryPanel(panel: RepositoryPanel): void { + this.removePanel(panel); + } + + moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void { + this.movePanel(from, to); + } + dispose(): void { this.disposables = dispose(this.disposables); super.dispose(); From a260c08a9fbb2bd150072bd104d988868efee4f0 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 21:20:05 +0200 Subject: [PATCH 30/67] splitview: remove exceptions --- src/vs/base/browser/ui/splitview/splitview2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 92a28e0cb26..28beb9d066f 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -199,7 +199,7 @@ export class SplitView implements IDisposable { resizeView(index: number, size: number): void { if (index < 0 || index >= this.viewItems.length - 1) { - throw new Error('Cant resize view'); + return; } this.resize(index, size - this.viewItems[index].size); @@ -279,7 +279,7 @@ export class SplitView implements IDisposable { } } - throw new Error('Sash not found'); + return 0; } dispose(): void { From c236bf4984939435de44e29a68f4f82ef7ab95fa Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 18 Sep 2017 22:20:53 +0200 Subject: [PATCH 31/67] wip: splitview tweaking --- src/vs/base/browser/ui/splitview/panelview.ts | 2 +- .../base/browser/ui/splitview/splitview2.ts | 46 ++++++++++----- .../scm/electron-browser/media/scmViewlet.css | 4 ++ .../parts/scm/electron-browser/scmViewlet.ts | 56 +++++++++++++------ 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 83e56ce5793..5757406a454 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -96,7 +96,7 @@ export abstract class Panel implements IView { constructor(options: IPanelOptions = {}) { this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; this.ariaHeaderLabel = options.ariaHeaderLabel || ''; - this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44; + this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; this.header = $('.panel-header'); } diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 28beb9d066f..3922591a095 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -39,6 +39,7 @@ interface IViewItem { explicitSize: number; container: HTMLElement; disposable: IDisposable; + layout(): void; } interface ISashItem { @@ -52,16 +53,6 @@ interface ISashDragState { sizes: number[]; } -function layoutViewItem(item: IViewItem, orientation: Orientation): void { - if (orientation === Orientation.VERTICAL) { - item.container.style.height = `${item.size}px`; - } else { - item.container.style.width = `${item.size}px`; - } - - item.view.layout(item.size, orientation); -} - export class SplitView implements IDisposable { private orientation: Orientation; @@ -98,8 +89,23 @@ export class SplitView implements IDisposable { const containerDisposable = toDisposable(() => this.el.removeChild(container)); const disposable = combinedDisposable([onChangeDisposable, containerDisposable]); + const layoutContainer = this.orientation === Orientation.VERTICAL + ? size => item.container.style.height = `${item.size}px` + : size => item.container.style.width = `${item.size}px`; + + let previousSize: number | undefined = undefined; + const layout = () => { + if (item.size === previousSize) { + return; + } + + layoutContainer(item.size); + item.view.layout(item.size, this.orientation); + previousSize = item.size; + }; + const explicitSize = size; - const item: IViewItem = { view, container, explicitSize, size, disposable }; + const item: IViewItem = { view, container, explicitSize, size, layout, disposable }; this.viewItems.splice(index, 0, item); // Add sash @@ -193,8 +199,14 @@ export class SplitView implements IDisposable { } private onViewChange(item: IViewItem): void { - item.size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - this.relayout(); + const index = this.viewItems.indexOf(item); + + if (index < 0 || index >= this.viewItems.length - 1) { + return; + } + + const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); + this.resize(index, size - item.size); } resizeView(index: number, size: number): void { @@ -219,6 +231,12 @@ export class SplitView implements IDisposable { const down = downIndexes.map(i => this.viewItems[i]); const downSizes = downIndexes.map(i => sizes[i]); + delta = clamp( + delta, + -upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0), + downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0) + ); + for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { const item = up[i]; const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); @@ -242,7 +260,7 @@ export class SplitView implements IDisposable { } private layoutViews(): void { - this.viewItems.forEach(item => layoutViewItem(item, this.orientation)); + this.viewItems.forEach(item => item.layout()); this.sashItems.forEach(item => item.sash.layout()); // Update sashes enablement diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index 137f20cd4b7..c8924adc9f8 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -145,4 +145,8 @@ .scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input { overflow-y: scroll; +} + +.scm-viewlet .split-view-view { + background: green; } \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 0f7d03fe781..ec75adceddd 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -12,7 +12,7 @@ import { chain } from 'vs/base/common/event'; import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; -import { Builder } from 'vs/base/browser/builder'; +import { Builder, Dimension } from 'vs/base/browser/builder'; import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/views2'; import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -68,11 +68,12 @@ class SCMMenuItemActionItem extends MenuItemActionItem { } } -interface IViewModel { +export interface IViewModel { + readonly height: number | undefined; + addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void; removeRepositoryPanel(panel: RepositoryPanel): void; moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void; - isRepositoryVisible(repository: ISCMRepository): boolean; toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); } @@ -236,9 +237,9 @@ class MainPanel extends ViewletPanel { this.list.splice(this.list.length, 0, [repository]); this.updateBodySize(); - if (this.repositories.length === 1) { - this.list.setSelection([0]); - } + // if (this.repositories.length === 1) { + this.list.setSelection(this.repositories.map((_, i) => i)); + // } } private onDidRemoveRepository(repository: ISCMRepository): void { @@ -254,16 +255,27 @@ class MainPanel extends ViewletPanel { } private onSelectionChange(e: IListEvent): void { - console.log(e.elements); + // Remove unselected panels + this.repositoryPanels + .filter(p => e.elements.every(r => p.repository !== r)) + .forEach(panel => this.viewModel.removeRepositoryPanel(panel)); - const toRemove = this.repositoryPanels.filter(p => e.elements.every(r => p.repository !== r)); - const toAdd = e.elements.filter(r => this.repositoryPanels.every(p => p.repository !== r)); + // Collect panels still selected + const repositoryPanels = this.repositoryPanels + .filter(p => e.elements.some(r => p.repository === r)); - console.log(toAdd, toRemove); + // Collect new selected panels + const newRepositoryPanels = e.elements + .filter(r => this.repositoryPanels.every(p => p.repository !== r)) + .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); + // Add new selected panels + const height = typeof this.viewModel.height === 'number' ? this.viewModel.height : 1000; + const size = (height - this.minimumSize) / e.elements.length; + console.log(this.viewModel.height, height, this.minimumBodySize, e.elements.length, size); + newRepositoryPanels.forEach(panel => this.viewModel.addRepositoryPanel(panel, size)); - // this.viewModel.addRepositoryPanel(); - // this.repositoryPanels + this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; } private updateBodySize(): void { @@ -483,8 +495,11 @@ export class RepositoryPanel extends ViewletPanel { @IInstantiationService protected instantiationService: IInstantiationService ) { super(repository.provider.label, {}, keybindingService, contextMenuService); - this.menus = instantiationService.createInstance(SCMMenus, repository.provider); + } + + render(container: HTMLElement): void { + super.render(container); this.menus.onDidChangeTitle(this.updateActions, this, this.disposables); } @@ -703,15 +718,15 @@ class InstallAdditionalSCMProvidersAction extends Action { } } -export class SCMViewlet extends PanelViewlet { +export class SCMViewlet extends PanelViewlet implements IViewModel { private menus: SCMMenus; - private mainPanel: MainPanel; - - // private repositoryToViewDescriptor = new Map(); private disposables: IDisposable[] = []; + private _height: number | undefined = undefined; + get height(): number | undefined { return this._height; } + constructor( @ITelemetryService telemetryService: ITelemetryService, @ISCMService protected scmService: ISCMService, @@ -835,7 +850,12 @@ export class SCMViewlet extends PanelViewlet { return new SCMMenuItemActionItem(action, this.keybindingService, this.messageService); } - addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void { + layout(dimension: Dimension): void { + super.layout(dimension); + this._height = dimension.height; + } + + addRepositoryPanel(panel: RepositoryPanel, size: number, index: number = this.length): void { this.addPanel(panel, size, index + 1); } From f351de8d11f14a498be3d96398fa62aa473090cd Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 07:54:06 +0200 Subject: [PATCH 32/67] splitview: round sizes --- src/vs/base/browser/ui/splitview/splitview2.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 3922591a095..e8ae8eeacdd 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -104,6 +104,7 @@ export class SplitView implements IDisposable { previousSize = item.size; }; + size = Math.round(size); const explicitSize = size; const item: IViewItem = { view, container, explicitSize, size, layout, disposable }; this.viewItems.splice(index, 0, item); @@ -214,6 +215,7 @@ export class SplitView implements IDisposable { return; } + size = Math.round(size); this.resize(index, size - this.viewItems[index].size); } From b6e9893b9bc9d1258ff0b3daf658575d561edf3d Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 07:54:17 +0200 Subject: [PATCH 33/67] remove log --- src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index ec75adceddd..58343b62ae3 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -272,7 +272,6 @@ class MainPanel extends ViewletPanel { // Add new selected panels const height = typeof this.viewModel.height === 'number' ? this.viewModel.height : 1000; const size = (height - this.minimumSize) / e.elements.length; - console.log(this.viewModel.height, height, this.minimumBodySize, e.elements.length, size); newRepositoryPanels.forEach(panel => this.viewModel.addRepositoryPanel(panel, size)); this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; From 63cf6afdbf3ea853cf83fa9058d4c44f4d7e83a8 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 11:34:26 +0200 Subject: [PATCH 34/67] splitview: fix layout code --- .../base/browser/ui/splitview/splitview2.ts | 96 +++++++++++------ .../browser/ui/splitview/splitview.test.ts | 100 +++++++----------- 2 files changed, 104 insertions(+), 92 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index e8ae8eeacdd..5c75416648d 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -36,7 +36,6 @@ interface ISashEvent { interface IViewItem { view: IView; size: number; - explicitSize: number; container: HTMLElement; disposable: IDisposable; layout(): void; @@ -51,6 +50,8 @@ interface ISashDragState { index: number; start: number; sizes: number[]; + minDelta: number; + maxDelta: number; } export class SplitView implements IDisposable { @@ -58,6 +59,7 @@ export class SplitView implements IDisposable { private orientation: Orientation; private el: HTMLElement; private size = 0; + private contentSize = 0; private viewItems: IViewItem[] = []; private sashItems: ISashItem[] = []; private sashDragState: ISashDragState; @@ -105,8 +107,7 @@ export class SplitView implements IDisposable { }; size = Math.round(size); - const explicitSize = size; - const item: IViewItem = { view, container, explicitSize, size, layout, disposable }; + const item: IViewItem = { view, container, size, layout, disposable }; this.viewItems.splice(index, 0, item); // Add sash @@ -129,7 +130,7 @@ export class SplitView implements IDisposable { } view.render(container, this.orientation); - this.relayoutPreferredSizes(); + this.relayout(); } removeView(index: number): void { @@ -148,7 +149,7 @@ export class SplitView implements IDisposable { sashItem.disposable.dispose(); } - this.relayoutPreferredSizes(); + this.relayout(); } moveView(from: number, to: number): void { @@ -169,54 +170,82 @@ export class SplitView implements IDisposable { this.layoutViews(); } - private relayoutPreferredSizes(): void { - this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize)); - this.relayout(); - } - private relayout(): void { - const previousSize = this.size; - this.size = this.viewItems.reduce((r, i) => r + i.size, 0); - this.layout(previousSize); + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + this.resize(this.viewItems.length - 1, this.contentSize - contentSize); } layout(size: number): void { - this.resize(this.viewItems.length - 1, size - this.size); - this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0)); + const previousSize = Math.max(this.size, this.contentSize); + this.size = size; + this.resize(this.viewItems.length - 1, size - previousSize); } private onSashStart({ sash, start }: ISashEvent): void { const index = firstIndex(this.sashItems, item => item.sash === sash); const sizes = this.viewItems.map(i => i.size); - this.sashDragState = { start, index, sizes }; + const upIndexes = range(index, -1); + const collapseUp = upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0); + const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0); + + const downIndexes = range(index + 1, this.viewItems.length); + const collapseDown = downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0); + const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0); + + const minDelta = -Math.min(collapseUp, expandDown); + const maxDelta = Math.min(collapseDown, expandUp); + + this.sashDragState = { start, index, sizes, minDelta, maxDelta }; } private onSashChange({ sash, current }: ISashEvent): void { - const { index, start, sizes } = this.sashDragState; + const { index, start, sizes, minDelta, maxDelta } = this.sashDragState; + const delta = clamp(current - start, minDelta, maxDelta); - this.resize(index, current - start, sizes); - this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size); + this.resize(index, delta, sizes); } private onViewChange(item: IViewItem): void { const index = this.viewItems.indexOf(item); - if (index < 0 || index >= this.viewItems.length - 1) { + if (index < 0 || index >= this.viewItems.length) { return; } const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); - this.resize(index, size - item.size); + item.size = size; + this.relayout(); } resizeView(index: number, size: number): void { - if (index < 0 || index >= this.viewItems.length - 1) { + if (index < 0 || index >= this.viewItems.length) { return; } + const item = this.viewItems[index]; size = Math.round(size); - this.resize(index, size - this.viewItems[index].size); + size = clamp(size, item.view.minimumSize, item.view.maximumSize); + let delta = size - item.size; + + if (delta !== 0 && index < this.viewItems.length - 1) { + const downIndexes = range(index + 1, this.viewItems.length); + const collapseDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0); + const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0); + const deltaDown = clamp(delta, -expandDown, collapseDown); + + this.resize(index, deltaDown); + delta -= deltaDown; + } + + if (delta !== 0 && index > 0) { + const upIndexes = range(index - 1, -1); + const collapseUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0); + const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0); + const deltaUp = clamp(-delta, -collapseUp, expandUp); + + this.resize(index - 1, deltaUp); + } } private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void { @@ -228,17 +257,10 @@ export class SplitView implements IDisposable { const upIndexes = range(index, -1); const up = upIndexes.map(i => this.viewItems[i]); const upSizes = upIndexes.map(i => sizes[i]); - const downIndexes = range(index + 1, this.viewItems.length); const down = downIndexes.map(i => this.viewItems[i]); const downSizes = downIndexes.map(i => sizes[i]); - delta = clamp( - delta, - -upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0), - downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0) - ); - for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { const item = up[i]; const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); @@ -258,6 +280,20 @@ export class SplitView implements IDisposable { } } + let contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + let emptyDelta = this.size - contentSize; + + for (let i = this.viewItems.length - 1; emptyDelta > 0 && i >= 0; i--) { + const item = this.viewItems[i]; + const size = clamp(item.size + emptyDelta, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - item.size; + + emptyDelta -= viewDelta; + item.size = size; + } + + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + this.layoutViews(); } diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index be73ee96d46..07fb5816104 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -82,7 +82,7 @@ suite('Splitview', () => { splitview.dispose(); }); - test('has views as sashes as children', () => { + test('has views and sashes as children', () => { const view1 = new TestView(20, 20); const view2 = new TestView(20, 20); const view3 = new TestView(20, 20); @@ -92,34 +92,34 @@ suite('Splitview', () => { splitview.addView(view2, 20); splitview.addView(view3, 20); - let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + let viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); assert.equal(viewQuery.length, 3, 'split view should have 3 views'); - let sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + let sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); assert.equal(sashQuery.length, 2, 'split view should have 2 sashes'); splitview.removeView(2); - viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); assert.equal(viewQuery.length, 2, 'split view should have 2 views'); - sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); assert.equal(sashQuery.length, 1, 'split view should have 1 sash'); splitview.removeView(0); - viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); assert.equal(viewQuery.length, 1, 'split view should have 1 view'); - sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); assert.equal(sashQuery.length, 0, 'split view should have no sashes'); splitview.removeView(0); - viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); assert.equal(viewQuery.length, 0, 'split view should have no views'); - sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); assert.equal(sashQuery.length, 0, 'split view should have no sashes'); splitview.dispose(); @@ -177,31 +177,6 @@ suite('Splitview', () => { view.dispose(); }); - test('respects preferred sizes with structural changes', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); - splitview.layout(200); - - splitview.addView(view1, 20); - assert.equal(view1.size, 200, 'view1 is stretched'); - - splitview.addView(view2, 20); - assert.equal(view1.size, 20, 'view1 size is restored'); - assert.equal(view2.size, 200 - 20, 'view2 is stretched'); - - splitview.addView(view3, 20); - assert.equal(view1.size, 20, 'view1 size is restored'); - assert.equal(view2.size, 20, 'view2 size is restored'); - assert.equal(view3.size, 160, 'view3 is stretched'); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); - }); - test('can resize views', () => { const view1 = new TestView(20, Number.POSITIVE_INFINITY); const view2 = new TestView(20, Number.POSITIVE_INFINITY); @@ -213,27 +188,27 @@ suite('Splitview', () => { splitview.addView(view2, 20); splitview.addView(view3, 20); - assert.equal(view1.size, 20, 'view1 size is the default'); - assert.equal(view2.size, 20, 'view2 size the the default'); - assert.equal(view3.size, 160, 'view3 is stretched'); + assert.equal(view1.size, 160, 'view1 is stretched'); + assert.equal(view2.size, 20, 'view2 size is 20'); + assert.equal(view3.size, 20, 'view3 size is 20'); splitview.resizeView(1, 40); - assert.equal(view1.size, 20, 'view1 is untouched'); + assert.equal(view1.size, 140, 'view1 is collapsed'); assert.equal(view2.size, 40, 'view2 is stretched'); - assert.equal(view3.size, 140, 'view3 is collapsed'); + assert.equal(view3.size, 20, 'view3 stays the same'); splitview.resizeView(0, 70); - assert.equal(view1.size, 70, 'view1 is stretched'); - assert.equal(view2.size, 20, 'view2 is collapsed'); - assert.equal(view3.size, 110, 'view3 is collapsed'); + assert.equal(view1.size, 70, 'view1 is collapsed'); + assert.equal(view2.size, 110, 'view2 is expanded'); + assert.equal(view3.size, 20, 'view3 stays the same'); - assert.throws(() => splitview.resizeView(2, 20)); + splitview.resizeView(2, 40); assert.equal(view1.size, 70, 'view1 stays the same'); - assert.equal(view2.size, 20, 'view2 stays the same'); - assert.equal(view3.size, 110, 'view3 stays the same'); + assert.equal(view2.size, 90, 'view2 is collapsed'); + assert.equal(view3.size, 40, 'view3 is stretched'); splitview.dispose(); view3.dispose(); @@ -252,32 +227,33 @@ suite('Splitview', () => { splitview.addView(view2, 20); splitview.addView(view3, 20); - assert.equal(view1.size, 20, 'view1 size is restored'); - assert.equal(view2.size, 20, 'view2 size is restored'); + assert.equal(view1.size, 160, 'view1 is stretched'); + assert.equal(view2.size, 20, 'view2 size is 20'); + assert.equal(view3.size, 20, 'view3 size is 20'); + + view1.maximumSize = 20; + + assert.equal(view1.size, 20, 'view1 is collapsed'); + assert.equal(view2.size, 20, 'view2 stays the same'); assert.equal(view3.size, 160, 'view3 is stretched'); - view3.maximumSize = 20; + view3.maximumSize = 40; assert.equal(view1.size, 20, 'view1 stays the same'); - assert.equal(view2.size, 160, 'view2 is stretched'); - assert.equal(view3.size, 20, 'view3 is collapsed'); + assert.equal(view2.size, 140, 'view2 is stretched'); + assert.equal(view3.size, 40, 'view3 is collapsed'); - view2.maximumSize = 40; + view2.maximumSize = 200; - assert.equal(view1.size, 140, 'view1 is stretched'); - assert.equal(view2.size, 40, 'view2 is collapsed'); - assert.equal(view3.size, 20, 'view3 is collapsed'); - - view3.maximumSize = 200; - - assert.equal(view1.size, 140, 'view1 stays the same'); - assert.equal(view2.size, 40, 'view2 stays the same'); - assert.equal(view3.size, 20, 'view3 stays the same'); + assert.equal(view1.size, 20, 'view1 stays the same'); + assert.equal(view2.size, 140, 'view2 stays the same'); + assert.equal(view3.size, 40, 'view3 stays the same'); + view3.maximumSize = Number.POSITIVE_INFINITY; view3.minimumSize = 100; - assert.equal(view1.size, 80, 'view1 is collapsed'); - assert.equal(view2.size, 20, 'view2 stays the same'); + assert.equal(view1.size, 20, 'view1 is collapsed'); + assert.equal(view2.size, 80, 'view2 is collapsed'); assert.equal(view3.size, 100, 'view3 is stretched'); splitview.dispose(); From d7e5744231e590807cb8e462fb3376ce7f89029b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 11:44:35 +0200 Subject: [PATCH 35/67] scm viewlet: multiselect --- src/vs/base/browser/ui/splitview/panelview.ts | 12 ++- .../workbench/browser/parts/views/views2.ts | 4 + .../scm/electron-browser/media/scmViewlet.css | 4 - .../parts/scm/electron-browser/scmViewlet.ts | 79 ++++--------------- 4 files changed, 32 insertions(+), 67 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 5757406a454..c2184f868c4 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -35,7 +35,7 @@ export abstract class Panel implements IView { private static HEADER_SIZE = 22; private _expanded: boolean; - private _headerVisible: boolean; + private _headerVisible = true; private _onDidChange = new Emitter(); private _minimumBodySize: number; private _maximumBodySize: number; @@ -359,6 +359,16 @@ export class PanelView implements IDisposable { this.splitview.moveView(fromIndex, toIndex); } + resizePanel(panel: Panel, size: number): void { + const index = firstIndex(this.panelItems, item => item.panel === panel); + + if (index === -1) { + return; + } + + this.splitview.resizeView(index, size); + } + layout(size: number): void { this.splitview.layout(size); } diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts index 115dda093f4..9cc25c52ab7 100644 --- a/src/vs/workbench/browser/parts/views/views2.ts +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -251,6 +251,10 @@ export class PanelViewlet extends Viewlet { this.panelview.movePanel(from, to); } + resizePanel(panel: ViewletPanel, size: number): void { + this.panelview.resizePanel(panel, size); + } + private updateViewHeaders(): void { if (this.isSingleView) { this.panelItems[0].panel.headerVisible = false; diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index c8924adc9f8..729f543af6f 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -146,7 +146,3 @@ .scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input { overflow-y: scroll; } - -.scm-viewlet .split-view-view { - background: green; -} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 58343b62ae3..5ada6ac838d 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -74,6 +74,8 @@ export interface IViewModel { addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void; removeRepositoryPanel(panel: RepositoryPanel): void; moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void; + resizeRepositoryPanel(panel: RepositoryPanel, size: number): void; + isRepositoryVisible(repository: ISCMRepository): boolean; toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); } @@ -237,9 +239,9 @@ class MainPanel extends ViewletPanel { this.list.splice(this.list.length, 0, [repository]); this.updateBodySize(); - // if (this.repositories.length === 1) { - this.list.setSelection(this.repositories.map((_, i) => i)); - // } + if (this.repositories.length === 1) { + this.list.setSelection([0]); + } } private onDidRemoveRepository(repository: ISCMRepository): void { @@ -270,11 +272,16 @@ class MainPanel extends ViewletPanel { .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); // Add new selected panels + newRepositoryPanels.forEach(panel => this.viewModel.addRepositoryPanel(panel, panel.minimumSize)); + this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; + + // Resize all panels equally const height = typeof this.viewModel.height === 'number' ? this.viewModel.height : 1000; const size = (height - this.minimumSize) / e.elements.length; - newRepositoryPanels.forEach(panel => this.viewModel.addRepositoryPanel(panel, size)); - this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; + for (const panel of this.repositoryPanels) { + this.viewModel.resizeRepositoryPanel(panel, size); + } } private updateBodySize(): void { @@ -429,37 +436,6 @@ class ProviderListDelegate implements IDelegate 0) { -// this.id = ProviderViewDescriptor.freeIds.shift(); -// } else { -// this.id = `scm${ProviderViewDescriptor.idCount++}`; -// } -// } - -// dispose(): void { -// ProviderViewDescriptor.freeIds.push(this.id); -// } -// } - function scmResourceIdentityProvider(r: ISCMResourceGroup | ISCMResource): string { if (isSCMResource(r)) { const group = r.resourceGroup; @@ -758,33 +734,8 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this.mainPanel = this.instantiationService.createInstance(MainPanel, this); this.addPanel(this.mainPanel, this.mainPanel.minimumSize); - - // this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - // this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - // this.scmService.repositories.forEach(p => this.onDidAddRepository(p)); - - // ViewsRegistry.registerViews([new ProvidersViewDescriptor()]); } - // private onDidAddRepository(repository: ISCMRepository): void { - // const viewDescriptor = new ProviderViewDescriptor(repository); - // this.repositoryToViewDescriptor.set(repository.provider.id, viewDescriptor); - - // ViewsRegistry.registerViews([viewDescriptor]); - // toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - // this.updateTitleArea(); - // } - - // private onDidRemoveRepository(repository: ISCMRepository): void { - // const viewDescriptor = this.repositoryToViewDescriptor.get(repository.provider.id); - // this.repositoryToViewDescriptor.delete(repository.provider.id); - // viewDescriptor.dispose(); - - // ViewsRegistry.deregisterViews([viewDescriptor.id], ViewLocation.SCM); - // toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - // this.updateTitleArea(); - // } - isRepositoryVisible(repository: ISCMRepository): boolean { // const view = this.repositoryToViewDescriptor.get(repository.provider.id); // return !!this.getView(view.id); @@ -854,7 +805,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this._height = dimension.height; } - addRepositoryPanel(panel: RepositoryPanel, size: number, index: number = this.length): void { + addRepositoryPanel(panel: RepositoryPanel, size: number, index: number = this.length - 1): void { this.addPanel(panel, size, index + 1); } @@ -866,6 +817,10 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this.movePanel(from, to); } + resizeRepositoryPanel(panel: RepositoryPanel, size: number): void { + this.resizePanel(panel, size); + } + dispose(): void { this.disposables = dispose(this.disposables); super.dispose(); From 8f22f8992da766d80e5dea488b2d8f479fe3da67 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 11:51:00 +0200 Subject: [PATCH 36/67] scm: remove checkboxes --- .../parts/scm/electron-browser/scmViewlet.ts | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 5ada6ac838d..a2935154f7f 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -75,9 +75,6 @@ export interface IViewModel { removeRepositoryPanel(panel: RepositoryPanel): void; moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void; resizeRepositoryPanel(panel: RepositoryPanel, size: number): void; - - isRepositoryVisible(repository: ISCMRepository): boolean; - toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean); } class ProvidersListDelegate implements IDelegate { @@ -91,15 +88,6 @@ class ProvidersListDelegate implements IDelegate { } } -interface RepositoryTemplateData { - checkbox: HTMLInputElement; - title: HTMLElement; - type: HTMLElement; - actionBar: ActionBar; - disposable: IDisposable; - templateDisposable: IDisposable; -} - class StatusBarAction extends Action { constructor( @@ -128,6 +116,14 @@ class StatusBarActionItem extends ActionItem { } } +interface RepositoryTemplateData { + title: HTMLElement; + type: HTMLElement; + actionBar: ActionBar; + disposable: IDisposable; + templateDisposable: IDisposable; +} + class ProviderRenderer implements IRenderer { readonly templateId = 'provider'; @@ -139,7 +135,6 @@ class ProviderRenderer implements IRenderer this.viewModel.toggleRepositoryVisibility(repository, templateData.checkbox.checked))); - // const disposables = commands.map(c => this.statusbarService.addEntry({ // text: c.title, // tooltip: `${repository.provider.label} - ${c.tooltip}`, @@ -736,17 +727,6 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this.addPanel(this.mainPanel, this.mainPanel.minimumSize); } - isRepositoryVisible(repository: ISCMRepository): boolean { - // const view = this.repositoryToViewDescriptor.get(repository.provider.id); - // return !!this.getView(view.id); - return true; - } - - toggleRepositoryVisibility(repository: ISCMRepository, visible: boolean): void { - // const view = this.repositoryToViewDescriptor.get(repository.provider.id); - // this.toggleViewVisibility(view.id, visible); - } - getOptimalWidth(): number { return 400; } From da54cb7c90cb16a77200df9d0f0d920794dc2654 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 12:08:29 +0200 Subject: [PATCH 37/67] scm: provider counts, styles --- .../scm/electron-browser/media/scmViewlet.css | 12 ++++++-- .../parts/scm/electron-browser/scmViewlet.ts | 30 ++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index 729f543af6f..8d89c1f1ce2 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -37,8 +37,12 @@ flex-shrink: 0; } -.scm-viewlet .scm-provider > .name { - flex: 1; +.scm-viewlet .scm-provider > .count { + margin: 0 0.5em; +} + +.scm-viewlet .scm-provider > .count.hidden { + display: none; } .scm-viewlet .scm-provider > .name > .type { @@ -146,3 +150,7 @@ .scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input { overflow-y: scroll; } + +.scm-viewlet .spacer { + flex: 1; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index a2935154f7f..450f7cf3d53 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -119,6 +119,8 @@ class StatusBarActionItem extends ActionItem { interface RepositoryTemplateData { title: HTMLElement; type: HTMLElement; + countContainer: HTMLElement; + count: CountBadge; actionBar: ActionBar; disposable: IDisposable; templateDisposable: IDisposable; @@ -130,7 +132,8 @@ class ProviderRenderer implements IRenderer new StatusBarActionItem(a as StatusBarAction) }); const disposable = EmptyDisposable; - const templateDisposable = combinedDisposable([actionBar]); + const templateDisposable = combinedDisposable([actionBar, badgeStyler]); - return { title, type, actionBar, disposable, templateDisposable }; + return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; } renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { @@ -168,17 +177,21 @@ class ProviderRenderer implements IRenderer dispose(actions); disposables.push({ dispose: disposeActions }); - const updateActions = () => { + const update = () => { disposeActions(); const commands = repository.provider.statusBarCommands || []; actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService))); templateData.actionBar.clear(); templateData.actionBar.push(actions); + + const count = repository.provider.count || 0; + toggleClass(templateData.countContainer, 'hidden', count === 0); + templateData.count.setCount(repository.provider.count); }; - repository.provider.onDidChange(updateActions, null, disposables); - updateActions(); + repository.provider.onDidChange(update, null, disposables); + update(); templateData.disposable = combinedDisposable(disposables); } @@ -200,7 +213,8 @@ class MainPanel extends ViewletPanel { @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, @ISCMService protected scmService: ISCMService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IThemeService private themeService: IThemeService ) { super(localize('scm providers', "Source Control Providers"), {}, keybindingService, contextMenuService); this.updateBodySize(); @@ -214,6 +228,8 @@ class MainPanel extends ViewletPanel { identityProvider: repository => repository.provider.id }); + this.disposables.push(this.list); + this.disposables.push(attachListStyler(this.list, this.themeService)); this.list.onSelectionChange(this.onSelectionChange, this, this.disposables); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); From 5527470e70ae47d07771a82688a46b24a391634c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 13:36:45 +0200 Subject: [PATCH 38/67] scm: proper add/remove repository views --- .../scm/electron-browser/media/scmViewlet.css | 2 +- .../parts/scm/electron-browser/scmViewlet.ts | 210 +++++++++++------- 2 files changed, 136 insertions(+), 76 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index 8d89c1f1ce2..a20ef64cf68 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -19,7 +19,7 @@ } .scm-viewlet:not(.empty) .empty-message, -.scm-viewlet.empty .monaco-split-view{ +.scm-viewlet.empty .monaco-panel-view { display: none; } diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 450f7cf3d53..db200324594 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -8,16 +8,16 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { chain } from 'vs/base/common/event'; +import Event, { Emitter, chain } from 'vs/base/common/event'; import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Builder, Dimension } from 'vs/base/browser/builder'; import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/views2'; -import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -68,13 +68,15 @@ class SCMMenuItemActionItem extends MenuItemActionItem { } } -export interface IViewModel { - readonly height: number | undefined; +export interface ISpliceEvent { + index: number; + deleteCount: number; + elements: T[]; +} - addRepositoryPanel(panel: RepositoryPanel, size: number, index?: number): void; - removeRepositoryPanel(panel: RepositoryPanel): void; - moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void; - resizeRepositoryPanel(panel: RepositoryPanel, size: number): void; +export interface IViewModel { + readonly repositories: ISCMRepository[]; + readonly onDidSplice: Event>; } class ProvidersListDelegate implements IDelegate { @@ -131,7 +133,6 @@ class ProviderRenderer implements IRenderer; - private repositories: ISCMRepository[] = []; - private repositoryPanels: RepositoryPanel[] = []; + + private _onSelectionChange = new Emitter(); + readonly onSelectionChange: Event = this._onSelectionChange.event; constructor( protected viewModel: IViewModel, @@ -220,9 +222,21 @@ class MainPanel extends ViewletPanel { this.updateBodySize(); } + private splice(index: number, deleteCount: number, repositories: ISCMRepository[] = []): void { + const wasEmpty = this.list.length === 0; + + this.list.splice(index, deleteCount, repositories); + this.updateBodySize(); + + // Automatically select the first one + if (wasEmpty && this.list.length > 0) { + this.list.setSelection([0]); + } + } + protected renderBody(container: HTMLElement): void { const delegate = new ProvidersListDelegate(); - const renderer = this.instantiationService.createInstance(ProviderRenderer, this.viewModel); + const renderer = this.instantiationService.createInstance(ProviderRenderer); this.list = new List(container, delegate, [renderer], { identityProvider: repository => repository.provider.id @@ -230,69 +244,17 @@ class MainPanel extends ViewletPanel { this.disposables.push(this.list); this.disposables.push(attachListStyler(this.list, this.themeService)); - this.list.onSelectionChange(this.onSelectionChange, this, this.disposables); - - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); + this.list.onSelectionChange(e => this._onSelectionChange.fire(e.elements), null, this.disposables); + this.viewModel.onDidSplice(({ index, deleteCount, elements }) => this.splice(index, deleteCount, elements), null, this.disposables); + this.splice(0, 0, this.viewModel.repositories); } protected layoutBody(size: number): void { this.list.layout(size); } - private onDidAddRepository(repository: ISCMRepository): void { - this.repositories.push(repository); - this.list.splice(this.list.length, 0, [repository]); - this.updateBodySize(); - - if (this.repositories.length === 1) { - this.list.setSelection([0]); - } - } - - private onDidRemoveRepository(repository: ISCMRepository): void { - const index = this.repositories.indexOf(repository); - - if (index === -1) { - return; - } - - this.repositories.splice(index, 1); - this.list.splice(index, 1); - this.updateBodySize(); - } - - private onSelectionChange(e: IListEvent): void { - // Remove unselected panels - this.repositoryPanels - .filter(p => e.elements.every(r => p.repository !== r)) - .forEach(panel => this.viewModel.removeRepositoryPanel(panel)); - - // Collect panels still selected - const repositoryPanels = this.repositoryPanels - .filter(p => e.elements.some(r => p.repository === r)); - - // Collect new selected panels - const newRepositoryPanels = e.elements - .filter(r => this.repositoryPanels.every(p => p.repository !== r)) - .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); - - // Add new selected panels - newRepositoryPanels.forEach(panel => this.viewModel.addRepositoryPanel(panel, panel.minimumSize)); - this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; - - // Resize all panels equally - const height = typeof this.viewModel.height === 'number' ? this.viewModel.height : 1000; - const size = (height - this.minimumSize) / e.elements.length; - - for (const panel of this.repositoryPanels) { - this.viewModel.resizeRepositoryPanel(panel, size); - } - } - private updateBodySize(): void { - const size = Math.min(5, this.repositories.length) * 22; + const size = Math.min(5, this.viewModel.repositories.length) * 22; this.minimumBodySize = size; this.maximumBodySize = size; } @@ -702,13 +664,22 @@ class InstallAdditionalSCMProvidersAction extends Action { export class SCMViewlet extends PanelViewlet implements IViewModel { + private el: HTMLElement; private menus: SCMMenus; - private mainPanel: MainPanel; + private mainPanel: MainPanel | null = null; + private mainPanelDisposable: IDisposable = EmptyDisposable; + private _repositories: ISCMRepository[] = []; + private repositoryPanels: RepositoryPanel[] = []; private disposables: IDisposable[] = []; + private _onDidSplice = new Emitter>(); + readonly onDidSplice: Event> = this._onDidSplice.event; + private _height: number | undefined = undefined; get height(): number | undefined { return this._height; } + get repositories(): ISCMRepository[] { return this._repositories; } + constructor( @ITelemetryService telemetryService: ITelemetryService, @ISCMService protected scmService: ISCMService, @@ -736,11 +707,70 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { async create(parent: Builder): TPromise { await super.create(parent); - parent.addClass('scm-viewlet'/* , 'empty' */); - // append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no source control providers active."))); + this.el = parent.getHTMLElement(); + addClass(this.el, 'scm-viewlet'); + addClass(this.el, 'empty'); + append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); - this.mainPanel = this.instantiationService.createInstance(MainPanel, this); - this.addPanel(this.mainPanel, this.mainPanel.minimumSize); + this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); + this.onDidChangeRepositories(); + } + + private onDidAddRepository(repository: ISCMRepository): void { + this.onDidChangeRepositories(); + + const index = this._repositories.length; + this._repositories.push(repository); + this._onDidSplice.fire({ index, deleteCount: 0, elements: [repository] }); + + if (!this.mainPanel) { + this.onSelectionChange(this.repositories); + } + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + this.onDidChangeRepositories(); + + const index = this._repositories.indexOf(repository); + + if (index === -1) { + return; + } + + this._repositories.splice(index, 1); + this._onDidSplice.fire({ index, deleteCount: 1, elements: [] }); + + if (!this.mainPanel) { + this.onSelectionChange(this.repositories); + } + } + + private onDidChangeRepositories(): void { + toggleClass(this.el, 'empty', this.scmService.repositories.length === 0); + + const shouldMainPanelBeVisible = this.scmService.repositories.length > 1; + + if (!!this.mainPanel === shouldMainPanelBeVisible) { + return; + } + + if (shouldMainPanelBeVisible) { + this.mainPanel = this.instantiationService.createInstance(MainPanel, this); + const selectionChangeDisposable = this.mainPanel.onSelectionChange(this.onSelectionChange, this); + this.addPanel(this.mainPanel, this.mainPanel.minimumSize, 0); + + this.mainPanelDisposable = toDisposable(() => { + this.removePanel(this.mainPanel); + selectionChangeDisposable.dispose(); + this.mainPanel.dispose(); + }); + } else { + this.mainPanelDisposable.dispose(); + this.mainPanelDisposable = EmptyDisposable; + this.mainPanel = null; + } } getOptimalWidth(): number { @@ -817,8 +847,38 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this.resizePanel(panel, size); } + private onSelectionChange(repositories: ISCMRepository[]): void { + // Remove unselected panels + this.repositoryPanels + .filter(p => repositories.every(r => p.repository !== r)) + .forEach(panel => this.removeRepositoryPanel(panel)); + + // Collect panels still selected + const repositoryPanels = this.repositoryPanels + .filter(p => repositories.some(r => p.repository === r)); + + // Collect new selected panels + const newRepositoryPanels = repositories + .filter(r => this.repositoryPanels.every(p => p.repository !== r)) + .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); + + // Add new selected panels + newRepositoryPanels.forEach(panel => this.addRepositoryPanel(panel, panel.minimumSize)); + this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; + + // Resize all panels equally + const height = typeof this.height === 'number' ? this.height : 1000; + const mainPanelHeight = this.mainPanel ? this.mainPanel.minimumSize : 0; + const size = (height - mainPanelHeight) / repositories.length; + + for (const panel of this.repositoryPanels) { + this.resizeRepositoryPanel(panel, size); + } + } + dispose(): void { this.disposables = dispose(this.disposables); + this.mainPanelDisposable.dispose(); super.dispose(); } } From a8eb4b710d7dfdee5abf1b12693257bbf901c504 Mon Sep 17 00:00:00 2001 From: Nguyen Long Nhat Date: Tue, 19 Sep 2017 20:33:16 +0700 Subject: [PATCH 39/67] Using node 7.9.0 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b552f3ee567..15af121f5c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,8 +27,8 @@ before_install: - git submodule update --init --recursive - git clone --depth 1 https://github.com/creationix/nvm.git ./.nvm - source ./.nvm/nvm.sh - - nvm install 7.4.0 - - nvm use 7.4.0 + - nvm install 7.9.0 + - nvm use 7.9.0 - npm config set python `which python` - npm install -g gulp - if [ $TRAVIS_OS_NAME == "linux" ]; then From 5a5050de6880db0ee905874dbb958769eff273fc Mon Sep 17 00:00:00 2001 From: Nguyen Long Nhat Date: Tue, 19 Sep 2017 20:34:06 +0700 Subject: [PATCH 40/67] Using node 7.9.0 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 49e80fed313..eaf86cb5b20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ environment: VSCODE_BUILD_VERBOSE: true install: - - ps: Install-Product node 7.4.0 x64 + - ps: Install-Product node 7.9.0 x64 - npm install -g npm@4 --silent - npm install -g gulp mocha --silent From d5d4deeaa7207b3ae9f40a6bffdbcda59ef1f186 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 19 Sep 2017 15:56:42 +0200 Subject: [PATCH 41/67] add _workbench.pickWorkspace command --- .../browser/actions/workspaceActions.ts | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 21a78d1b501..3ab372f4166 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -15,15 +15,18 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import URI from 'vs/base/common/uri'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isLinux } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/paths'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { isParent } from 'vs/platform/files/common/files'; +import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; +import { isParent, FileKind } from 'vs/platform/files/common/files'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IQuickOpenService, IFilePickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; export class OpenFolderAction extends Action { @@ -282,4 +285,65 @@ export class OpenWorkspaceConfigFileAction extends Action { public run(): TPromise { return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }); } -} \ No newline at end of file +} + +export const PICK_WORKSPACE_COMMAND = '_workbench.pickWorkspace'; + +CommandsRegistry.registerCommand(PICK_WORKSPACE_COMMAND, function (accessor: ServicesAccessor, args?: [IPickOptions, CancellationToken]) { + const contextService = accessor.get(IWorkspaceContextService); + const quickOpenService = accessor.get(IQuickOpenService); + const environmentService = accessor.get(IEnvironmentService); + + const folders = contextService.getWorkspace().folders; + if (!folders.length) { + return void 0; + } + + const folderPicks = folders.map(folder => { + return { + label: folder.name, + description: getPathLabel(dirname(folder.uri.fsPath), void 0, environmentService), + folder, + resource: folder.uri, + fileKind: FileKind.ROOT_FOLDER + } as IFilePickOpenEntry; + }); + + let options: IPickOptions; + if (args) { + options = args[0]; + } + + if (!options) { + options = Object.create(null); + } + + if (!options.autoFocus) { + options.autoFocus = { autoFocusFirstEntry: true }; + } + + if (!options.placeHolder) { + options.placeHolder = nls.localize('workspaceFolderPickerPlaceholder', "Select workspace folder"); + } + + if (typeof options.matchOnDescription !== 'boolean') { + options.matchOnDescription = true; + } + + let token: CancellationToken; + if (args) { + token = args[1]; + } + + if (!token) { + token = CancellationToken.None; + } + + return quickOpenService.pick(folderPicks, options, token).then(pick => { + if (!pick) { + return void 0; + } + + return folders[folderPicks.indexOf(pick)]; + }); +}); \ No newline at end of file From c1f792ed4ce7106c438e8dd1b19c11a7d5ed0f33 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:12:40 +0200 Subject: [PATCH 42/67] scm: show actions, display focus --- .../base/browser/ui/splitview/panelview.css | 61 ++++++++++++------- src/vs/base/browser/ui/splitview/panelview.ts | 7 ++- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.css b/src/vs/base/browser/ui/splitview/panelview.css index b22f256de7e..6dfe2379b16 100644 --- a/src/vs/base/browser/ui/splitview/panelview.css +++ b/src/vs/base/browser/ui/splitview/panelview.css @@ -22,28 +22,8 @@ text-transform: uppercase; padding-left: 20px; overflow: hidden; -} - -/* Bold font style does not go well with CJK fonts */ -.monaco-panel-view:lang(zh-Hans) .panel > .panel-header, -.monaco-panel-view:lang(zh-Hant) .panel > .panel-header, -.monaco-panel-view:lang(ja) .panel > .panel-header, -.monaco-panel-view:lang(ko) .panel > .panel-header { - font-weight: normal; -} - -.monaco-panel-view .panel > .panel-header.hidden { - display: none; -} - -.monaco-panel-view .panel > .panel-body { - overflow: hidden; - flex: 1; -} - -.monaco-panel-view .panel > .panel-header { + display: flex; cursor: pointer; - /* background: rgba(128, 128, 128, 0.2); */ } .monaco-panel-view .panel > .panel-header { @@ -64,4 +44,43 @@ .vs-dark .monaco-panel-view .panel > .panel-header.expanded { background-image: url('arrow-expand-dark.svg'); +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel > .panel-header > .actions { + display: none; + flex: 1; +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel:hover > .panel-header > .actions, +.monaco-panel-view .panel > .panel-header.focused > .actions { + display: initial; +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel > .panel-header > .actions .action-label { + width: 28px; + height: 22px; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; + margin-right: 0; +} + +/* Bold font style does not go well with CJK fonts */ +.monaco-panel-view:lang(zh-Hans) .panel > .panel-header, +.monaco-panel-view:lang(zh-Hant) .panel > .panel-header, +.monaco-panel-view:lang(ja) .panel > .panel-header, +.monaco-panel-view:lang(ko) .panel > .panel-header { + font-weight: normal; +} + +.monaco-panel-view .panel > .panel-header.hidden { + display: none; +} + +.monaco-panel-view .panel > .panel-body { + overflow: hidden; + flex: 1; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index c2184f868c4..8b02b510168 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -11,7 +11,7 @@ import Event, { Emitter, chain } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { $, append, addClass, removeClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { firstIndex } from 'vs/base/common/arrays'; import { Color, RGBA } from 'vs/base/common/color'; import { SplitView, IView } from './splitview2'; @@ -137,6 +137,11 @@ export abstract class Panel implements IView { this.header.setAttribute('role', 'toolbar'); this.header.setAttribute('aria-label', this.ariaHeaderLabel); this.renderHeader(this.header); + + const focusTracker = trackFocus(this.header); + focusTracker.addFocusListener(() => addClass(this.header, 'focused')); + focusTracker.addBlurListener(() => removeClass(this.header, 'focused')); + this.updateHeader(); const onHeaderKeyDown = chain(domEvent(this.header, 'keydown')) From 3c81d583067e7ad63a97f404688d4e3eed52852c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:20:07 +0200 Subject: [PATCH 43/67] splitview: dont save layout size --- src/vs/base/browser/ui/splitview/splitview2.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index 5c75416648d..b2deecf27f9 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -95,15 +95,9 @@ export class SplitView implements IDisposable { ? size => item.container.style.height = `${item.size}px` : size => item.container.style.width = `${item.size}px`; - let previousSize: number | undefined = undefined; const layout = () => { - if (item.size === previousSize) { - return; - } - layoutContainer(item.size); item.view.layout(item.size, this.orientation); - previousSize = item.size; }; size = Math.round(size); From b2807a92c2644e2b5519779101264b69fb34fc3f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:20:28 +0200 Subject: [PATCH 44/67] scm: typo --- src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index db200324594..a429b895d7d 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -418,7 +418,7 @@ function scmResourceIdentityProvider(r: ISCMResourceGroup | ISCMResource): strin export class RepositoryPanel extends ViewletPanel { - private cachedHeight: number | undefined; + private cachedHeight: number | undefined = undefined; private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; @@ -531,7 +531,7 @@ export class RepositoryPanel extends ViewletPanel { } layoutBody(height: number = this.cachedHeight): void { - if (!height === undefined) { + if (height === undefined) { return; } @@ -698,7 +698,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { @IStorageService storageService: IStorageService, @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: false }, telemetryService, themeService); + super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: true }, telemetryService, themeService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this.menus.onDidChangeTitle(this.updateTitleArea, this, this.disposables); From 7195502ec7fda9f52b820074a83b701b494761cb Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:28:35 +0200 Subject: [PATCH 45/67] scm: more actions --- .../parts/scm/electron-browser/scmViewlet.ts | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index a429b895d7d..7cb6a887331 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -779,20 +779,20 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { getTitle(): string { const title = localize('source control', "Source Control"); - // const views = ViewsRegistry.getViews(ViewLocation.SCM); - // if (views.length === 1) { - // const view = views[0]; - // return localize('viewletTitle', "{0}: {1}", title, view.name); - // } else { - return title; - // } + if (this.repositories.length === 1) { + const [repository] = this.repositories; + return localize('viewletTitle', "{0}: {1}", title, repository.provider.label); + } else { + return title; + } } getActions(): IAction[] { - // if (this.isSingleView) { - // return this.views[0].getActions(); - // } + if (this.isSingleView) { + const [panel] = this.repositoryPanels; + return panel.getActions(); + } return this.menus.getTitleActions(); } @@ -800,18 +800,20 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { getSecondaryActions(): IAction[] { let result: IAction[]; - // if (this.isSingleView) { - // result = [ - // ...this.views[0].getSecondaryActions(), - // new Separator() - // ]; - // } else { - result = this.menus.getTitleSecondaryActions(); + if (this.isSingleView) { + const [panel] = this.repositoryPanels; - if (result.length > 0) { - result.push(new Separator()); + result = [ + ...panel.getSecondaryActions(), + new Separator() + ]; + } else { + result = this.menus.getTitleSecondaryActions(); + + if (result.length > 0) { + result.push(new Separator()); + } } - // } result.push(this.instantiationService.createInstance(InstallAdditionalSCMProvidersAction)); @@ -831,27 +833,11 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this._height = dimension.height; } - addRepositoryPanel(panel: RepositoryPanel, size: number, index: number = this.length - 1): void { - this.addPanel(panel, size, index + 1); - } - - removeRepositoryPanel(panel: RepositoryPanel): void { - this.removePanel(panel); - } - - moveRepositoryPanel(from: RepositoryPanel, to: RepositoryPanel): void { - this.movePanel(from, to); - } - - resizeRepositoryPanel(panel: RepositoryPanel, size: number): void { - this.resizePanel(panel, size); - } - private onSelectionChange(repositories: ISCMRepository[]): void { // Remove unselected panels this.repositoryPanels .filter(p => repositories.every(r => p.repository !== r)) - .forEach(panel => this.removeRepositoryPanel(panel)); + .forEach(panel => this.removePanel(panel)); // Collect panels still selected const repositoryPanels = this.repositoryPanels @@ -863,8 +849,11 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); // Add new selected panels - newRepositoryPanels.forEach(panel => this.addRepositoryPanel(panel, panel.minimumSize)); this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; + newRepositoryPanels.forEach(panel => { + this.addPanel(panel, panel.minimumSize, this.length); + panel.repository.focus(); + }); // Resize all panels equally const height = typeof this.height === 'number' ? this.height : 1000; @@ -872,7 +861,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { const size = (height - mainPanelHeight) / repositories.length; for (const panel of this.repositoryPanels) { - this.resizeRepositoryPanel(panel, size); + this.resizePanel(panel, size); } } From 8631b22498653dc1079cbfff2be482181857a5cc Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 19 Sep 2017 11:30:14 -0400 Subject: [PATCH 46/67] Closes #27408 - Adds ViewColumn.Active --- .../vscode-api-tests/src/window.test.ts | 20 +++++++++++++++++++ src/vs/vscode.d.ts | 1 + .../api/node/extHostTypeConverters.ts | 2 ++ src/vs/workbench/api/node/extHostTypes.ts | 1 + 4 files changed, 24 insertions(+) diff --git a/extensions/vscode-api-tests/src/window.test.ts b/extensions/vscode-api-tests/src/window.test.ts index a9d78e8cad3..275ac5ee5ca 100644 --- a/extensions/vscode-api-tests/src/window.test.ts +++ b/extensions/vscode-api-tests/src/window.test.ts @@ -138,6 +138,26 @@ suite('window namespace tests', () => { assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.One); }); + test('issue #27408 - showTextDocument & vscode.diff always default to ViewColumn.One', async () => { + const [docA, docB, docC] = await Promise.all([ + workspace.openTextDocument(await createRandomFile()), + workspace.openTextDocument(await createRandomFile()), + workspace.openTextDocument(await createRandomFile()) + ]); + + await window.showTextDocument(docA, ViewColumn.One); + await window.showTextDocument(docB, ViewColumn.Two); + + assert.ok(window.activeTextEditor); + assert.ok(window.activeTextEditor!.document === docB); + assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + + await window.showTextDocument(docC, ViewColumn.Active); + + assert.ok(window.activeTextEditor!.document === docC); + assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + }); + test('issue #5362 - Incorrect TextEditor passed by onDidChangeTextEditorSelection', (done) => { const file10Path = join(workspace.rootPath || '', './10linefile.ts'); const file30Path = join(workspace.rootPath || '', './30linefile.ts'); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 92310b76a73..f955fbbc245 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3311,6 +3311,7 @@ declare module 'vscode' { * used to show editors side by side. */ export enum ViewColumn { + Active = -1, One = 1, Two = 2, Three = 3 diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 0a8c0bcb019..fa732f92951 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -115,6 +115,8 @@ export function fromViewColumn(column?: vscode.ViewColumn): EditorPosition { editorColumn = EditorPosition.TWO; } else if (column === types.ViewColumn.Three) { editorColumn = EditorPosition.THREE; + } else if (column === types.ViewColumn.Active) { + editorColumn = undefined; } return editorColumn; } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index a9b213648b8..b6d063810b6 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -953,6 +953,7 @@ export class CompletionList { } export enum ViewColumn { + Active = -1, One = 1, Two = 2, Three = 3 From 1b265cf97411b57d8329854692f52a11dba32e2b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:54:04 +0200 Subject: [PATCH 47/67] fix npe --- src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 7cb6a887331..c88fb426b53 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -789,7 +789,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { } getActions(): IAction[] { - if (this.isSingleView) { + if (this.isSingleView && this.repositories.length === 1) { const [panel] = this.repositoryPanels; return panel.getActions(); } @@ -800,7 +800,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { getSecondaryActions(): IAction[] { let result: IAction[]; - if (this.isSingleView) { + if (this.isSingleView && this.repositories.length === 1) { const [panel] = this.repositoryPanels; result = [ From 6e48dd0fe8f7614bd32532edbcd067703109e16b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 19 Sep 2017 17:54:25 +0200 Subject: [PATCH 48/67] :lipstick: --- src/vs/workbench/browser/parts/views/views2.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts index 9cc25c52ab7..1721592a293 100644 --- a/src/vs/workbench/browser/parts/views/views2.ts +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -129,6 +129,7 @@ export class PanelViewlet extends Viewlet { private panelItems: IViewletPanelItem[] = []; private panelview: PanelView; + // TODO@Joao make this into method so people can override it protected get isSingleView(): boolean { return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1; } From bef99125c71912803aaff7658eba0d8be597bd86 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 19 Sep 2017 18:05:51 +0200 Subject: [PATCH 49/67] :lipstick: --- .../services/workspace/node/workspaceEditingService.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 173f8e1d103..e1789dfa8af 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -99,15 +99,9 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } private doSetFolders(folders: IStoredWorkspaceFolder[]): TPromise { - if (folders.length) { - const workspace = this.contextService.getWorkspace(); + const workspace = this.contextService.getWorkspace(); - return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: folders }, true); - } else { - // TODO: Sandeep - Removing all folders? - } - - return TPromise.as(void 0); + return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: folders }, true); } private isSupported(): boolean { From c19f1d76e00c4ee87618e67db224bbd037681362 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 19 Sep 2017 18:25:34 +0200 Subject: [PATCH 50/67] pickWorkspace => pickWorkspaceFolder --- src/vs/workbench/browser/actions/workspaceActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 3ab372f4166..58f09abc65b 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -287,9 +287,9 @@ export class OpenWorkspaceConfigFileAction extends Action { } } -export const PICK_WORKSPACE_COMMAND = '_workbench.pickWorkspace'; +export const PICK_WORKSPACE_FOLDER_COMMAND = '_workbench.pickWorkspaceFolder'; -CommandsRegistry.registerCommand(PICK_WORKSPACE_COMMAND, function (accessor: ServicesAccessor, args?: [IPickOptions, CancellationToken]) { +CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND, function (accessor: ServicesAccessor, args?: [IPickOptions, CancellationToken]) { const contextService = accessor.get(IWorkspaceContextService); const quickOpenService = accessor.get(IQuickOpenService); const environmentService = accessor.get(IEnvironmentService); From 933e0a8127f508ceec2cd446a85d6e7454ae22b5 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 19 Sep 2017 11:03:02 -0700 Subject: [PATCH 51/67] Fix markdown table of contents name for strings like # ff Fixes #34644 --- extensions/markdown/src/tableOfContentsProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/markdown/src/tableOfContentsProvider.ts b/extensions/markdown/src/tableOfContentsProvider.ts index 26e64e4d837..c93dd25f6e7 100644 --- a/extensions/markdown/src/tableOfContentsProvider.ts +++ b/extensions/markdown/src/tableOfContentsProvider.ts @@ -79,7 +79,7 @@ export class TableOfContentsProvider { } private static getHeaderText(header: string): string { - return header.replace(/^\s*#+\s*(.*?)\s*\1*$/, (_, word) => `${word.trim()}`); + return header.replace(/^\s*#+\s*(.*?)\s*#*$/, (_, word) => word.trim()); } public static slugify(header: string): string { From 4f599ae71d21a58c6457e031e077e2057686f5be Mon Sep 17 00:00:00 2001 From: Oliver Joseph Ash Date: Tue, 19 Sep 2017 20:41:19 +0100 Subject: [PATCH 52/67] Update .tsx import statement snippet to be consistent with .ts (#34653) Fixes https://github.com/Microsoft/vscode/issues/34646 --- extensions/typescript/snippets/typescriptreact.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/typescript/snippets/typescriptreact.json b/extensions/typescript/snippets/typescriptreact.json index c0d38f23c18..894d6177276 100644 --- a/extensions/typescript/snippets/typescriptreact.json +++ b/extensions/typescript/snippets/typescriptreact.json @@ -50,7 +50,7 @@ "Import external module.": { "prefix": "import statement", "body": [ - "import ${1:name} = require('$0');" + "import { $0 } from \"${1:module}\";" ], "description": "Import external module." }, From e090c114142785d054810fa197ac57a5b18b7b24 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 19 Sep 2017 22:57:05 +0200 Subject: [PATCH 53/67] Implement #34659 --- .../common/extensionManagement.ts | 8 +- .../node/extensionManagementService.ts | 176 ++++++++---------- .../node/extensionsWorkbenchService.ts | 10 +- 3 files changed, 93 insertions(+), 101 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 24d82c47bb5..b68462db6f9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -228,12 +228,18 @@ export interface InstallExtensionEvent { gallery?: IGalleryExtension; } +export enum ErrorCode { + OBSOLETE = 1, + GALLERY, + LOCAL +} + export interface DidInstallExtensionEvent { id: string; zipPath?: string; gallery?: IGalleryExtension; local?: ILocalExtension; - error?: Error; + error?: ErrorCode; } export interface DidUninstallExtensionEvent { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 24f88da7d2a..ce0bae03b33 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -18,7 +18,8 @@ import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IExtensionManifest, IGalleryMetadata, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType, - StatisticType + StatisticType, + ErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest, getGalleryExtensionIdFromLocal, getIdAndVersionFromLocalExtensionId, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from '../common/extensionNls'; @@ -68,6 +69,12 @@ function readManifest(extensionPath: string): TPromise<{ manifest: IExtensionMan }); } +interface InstallableExtension { + zipPath: string; + id: string; + metadata: IGalleryMetadata; +} + export class ExtensionManagementService implements IExtensionManagementService { _serviceBrand: any; @@ -107,12 +114,12 @@ export class ExtensionManagementService implements IExtensionManagementService { return this.isObsolete(id).then(isObsolete => { if (isObsolete) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name))); + return TPromise.wrapError(new Error(nls.localize('restartCodeLocal', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name))); } this._onInstallExtension.fire({ id, zipPath }); - return this.installExtension(zipPath, id) + return this.installExtension({ zipPath, id, metadata: null }) .then( local => this._onDidInstallExtension.fire({ id, zipPath, local }), error => { this._onDidInstallExtension.fire({ id, zipPath, error }); return TPromise.wrapError(error); } @@ -122,91 +129,83 @@ export class ExtensionManagementService implements IExtensionManagementService { } installFromGallery(extension: IGalleryExtension): TPromise { - const id = getLocalExtensionIdFromGallery(extension, extension.version); + return this.prepareAndCollectExtensionsToInstall(extension) + .then(extensionsToInstall => this.downloadAndInstallExtensions(extensionsToInstall) + .then(local => this.onDidInstallExtensions(extensionsToInstall, local))); + } - return this.isObsolete(id).then(isObsolete => { - if (isObsolete) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name))); - } - this._onInstallExtension.fire({ id, gallery: extension }); - return this.installCompatibleVersion(extension, true) + private prepareAndCollectExtensionsToInstall(extension: IGalleryExtension): TPromise { + this.onInstallExtensions([extension]); + return this.collectExtensionsToInstall(extension) + .then( + extensionsToInstall => this.checkForObsolete(extensionsToInstall) .then( - local => this._onDidInstallExtension.fire({ id, local, gallery: extension }), - error => { - this._onDidInstallExtension.fire({ id, gallery: extension, error }); - return TPromise.wrapError(error); - } - ); - }); - } - - private installCompatibleVersion(extension: IGalleryExtension, installDependencies: boolean): TPromise { - return this.galleryService.loadCompatibleVersion(extension) - .then(compatibleVersion => this.getDependenciesToInstall(extension, installDependencies) - .then(dependencies => dependencies.length ? this.installWithDependencies(compatibleVersion) : this.downloadAndInstall(compatibleVersion))); - } - - private getDependenciesToInstall(extension: IGalleryExtension, checkDependecies: boolean): TPromise { - if (!checkDependecies) { - return TPromise.wrap([]); - } - // Filter out self - const dependencies = extension.properties.dependencies ? extension.properties.dependencies.filter(id => id !== extension.id) : []; - if (!dependencies.length) { - return TPromise.wrap([]); - } - // Filter out installed dependencies - return this.getInstalled().then(installed => { - return dependencies.filter(dep => installed.every(i => `${i.manifest.publisher}.${i.manifest.name}` !== dep)); - }); - } - - private installWithDependencies(extension: IGalleryExtension): TPromise { - return this.galleryService.getAllDependencies(extension) - .then(allDependencies => this.filterDependenciesToInstall(extension, allDependencies)) - .then(toInstall => this.filterObsolete(...toInstall.map(i => getLocalExtensionIdFromGallery(i, i.version))) - .then((obsolete) => { - if (obsolete.length) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name))); + extensionsToInstall => { + if (extensionsToInstall.length > 1) { + this.onInstallExtensions(extensionsToInstall.slice(1)); } - return this.bulkInstallWithDependencies(extension, toInstall); - }) + return extensionsToInstall; + }, + error => this.onDidInstallExtensions([extension], null, ErrorCode.OBSOLETE, error) + ), + error => this.onDidInstallExtensions([extension], null, ErrorCode.GALLERY, error) ); } - private bulkInstallWithDependencies(extension: IGalleryExtension, dependecies: IGalleryExtension[]): TPromise { - for (const dependency of dependecies) { - const id = getLocalExtensionIdFromGallery(dependency, dependency.version); - this._onInstallExtension.fire({ id, gallery: dependency }); - } - return this.downloadAndInstall(extension) - .then(localExtension => { - return TPromise.join(dependecies.map((dep) => this.installCompatibleVersion(dep, false))) - .then(installedLocalExtensions => { - for (const installedLocalExtension of installedLocalExtensions) { - const gallery = this.getGalleryExtensionForLocalExtension(dependecies, installedLocalExtension); - this._onDidInstallExtension.fire({ id: installedLocalExtension.id, local: installedLocalExtension, gallery }); - } - return localExtension; - }, error => { - return this.rollback(localExtension, dependecies).then(() => { - return TPromise.wrapError(Array.isArray(error) ? error[error.length - 1] : error); - }); - }); - }) - .then(localExtension => localExtension, error => { - for (const dependency of dependecies) { - this._onDidInstallExtension.fire({ id: getLocalExtensionIdFromGallery(dependency, dependency.version), gallery: dependency, error }); - } - return TPromise.wrapError(error); - }); + private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise { + return TPromise.join(extensions.map(extensionToInstall => this.downloadInstallableExtension(extensionToInstall))) + .then( + installableExtensions => TPromise.join(installableExtensions.map(installableExtension => this.installExtension(installableExtension))) + .then(null, error => this.rollback(extensions).then(() => this.onDidInstallExtensions(extensions, null, ErrorCode.LOCAL, error))), + error => this.onDidInstallExtensions(extensions, null, ErrorCode.GALLERY, error)); } - private rollback(localExtension: ILocalExtension, dependecies: IGalleryExtension[]): TPromise { - return this.doUninstall(localExtension) - .then(() => this.filterOutUninstalled(dependecies)) - .then(installed => TPromise.join(installed.map((i) => this.doUninstall(i)))) - .then(() => null); + private collectExtensionsToInstall(extension: IGalleryExtension): TPromise { + return this.galleryService.loadCompatibleVersion(extension) + .then(extensionToInstall => this.galleryService.getAllDependencies(extension) + .then(allDependencies => this.filterDependenciesToInstall(extension, allDependencies)) + .then(dependenciesToInstall => [extensionToInstall, ...dependenciesToInstall])); + } + + private checkForObsolete(extensionsToInstall: IGalleryExtension[]): TPromise { + return this.filterObsolete(...extensionsToInstall.map(i => getLocalExtensionIdFromGallery(i, i.version))) + .then(obsolete => obsolete.length ? TPromise.wrapError(new Error(nls.localize('restartCodeGallery', "Please restart Code before reinstalling."))) : extensionsToInstall); + } + + private downloadInstallableExtension(extension: IGalleryExtension): TPromise { + const id = getLocalExtensionIdFromGallery(extension, extension.version); + const metadata = { + id: extension.uuid, + publisherId: extension.publisherId, + publisherDisplayName: extension.publisherDisplayName, + }; + return this.galleryService.download(extension) + .then(zipPath => validate(zipPath).then(() => ({ zipPath, id, metadata }))); + } + + private rollback(extensions: IGalleryExtension[]): TPromise { + return this.filterOutUninstalled(extensions) + .then(installed => TPromise.join(installed.map(local => this.uninstallExtension(local.id)))) + .then(() => null, () => null); + } + + private onInstallExtensions(extensions: IGalleryExtension[]): void { + for (const extension of extensions) { + const id = getLocalExtensionIdFromGallery(extension, extension.version); + this._onInstallExtension.fire({ id, gallery: extension }); + } + } + + private onDidInstallExtensions(extensions: IGalleryExtension[], local: ILocalExtension[], errorCode?: ErrorCode, error?: any): TPromise { + extensions.forEach((gallery, index) => { + const id = getLocalExtensionIdFromGallery(gallery, gallery.version); + if (errorCode) { + this._onDidInstallExtension.fire({ id, gallery, error: errorCode }); + } else { + this._onDidInstallExtension.fire({ id, gallery, local: local[index] }); + } + }); + return error ? TPromise.wrapError(Array.isArray(error) ? this.joinErrors(error) : error) : TPromise.as(null); } private filterDependenciesToInstall(extension: IGalleryExtension, dependencies: IGalleryExtension[]): TPromise { @@ -232,20 +231,7 @@ export class ExtensionManagementService implements IExtensionManagementService { return filtered.length ? filtered[0] : null; } - private downloadAndInstall(extension: IGalleryExtension): TPromise { - const id = getLocalExtensionIdFromGallery(extension, extension.version); - const metadata = { - id: extension.uuid, - publisherId: extension.publisherId, - publisherDisplayName: extension.publisherDisplayName, - }; - - return this.galleryService.download(extension) - .then(zipPath => validate(zipPath).then(() => zipPath)) - .then(zipPath => this.installExtension(zipPath, id, metadata)); - } - - private installExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise { + private installExtension({ zipPath, id, metadata }: InstallableExtension): TPromise { const extensionPath = path.join(this.extensionsPath, id); return pfs.rimraf(extensionPath).then(() => { @@ -291,7 +277,7 @@ export class ExtensionManagementService implements IExtensionManagementService { } return errors.reduce((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}\n${currentValue instanceof Error ? currentValue.message : currentValue}`); + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); }, new Error('')); } diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 9cebb13bc64..d05db9e48fe 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -20,7 +20,7 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, IExtensionManifest, - InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionTipsService + InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionTipsService, ErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionIdFromLocal, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -725,7 +725,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } if (extension.gallery) { // Report telemetry only for gallery extensions - this.reportTelemetry(installing, !error); + this.reportTelemetry(installing, error); } } this._onChange.fire(); @@ -759,7 +759,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } if (!error) { - this.reportTelemetry(uninstalling, true); + this.reportTelemetry(uninstalling); } this._onChange.fire(); @@ -789,12 +789,12 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { return local ? ExtensionState.Installed : ExtensionState.Uninstalled; } - private reportTelemetry(active: IActiveExtension, success: boolean): void { + private reportTelemetry(active: IActiveExtension, errorcode?: ErrorCode): void { const data = active.extension.telemetryData; const duration = new Date().getTime() - active.start.getTime(); const eventName = toTelemetryEventName(active.operation); - this.telemetryService.publicLog(eventName, assign(data, { success, duration })); + this.telemetryService.publicLog(eventName, assign(data, { success: !errorcode, duration, errorcode })); } private onError(err: any): void { From a79a99078626299b81e29f695fdf9cca562749ec Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 19 Sep 2017 23:06:13 +0200 Subject: [PATCH 54/67] #34659 Use error code in uninstall event --- .../extensionManagement/common/extensionManagement.ts | 2 +- .../extensionManagement/node/extensionManagementService.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index b68462db6f9..df42ecd966c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -244,7 +244,7 @@ export interface DidInstallExtensionEvent { export interface DidUninstallExtensionEvent { id: string; - error?: Error; + error?: ErrorCode; } export interface IExtensionManagementService { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index ce0bae03b33..ae321626480 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -286,7 +286,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, error); + this.postUninstallExtension(extension, ErrorCode.LOCAL); return TPromise.wrapError(error); }); } @@ -402,7 +402,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.uninstallExtension(extension.id)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, error); + this.postUninstallExtension(extension, ErrorCode.LOCAL); return TPromise.wrapError(error); }); } @@ -421,7 +421,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.unsetObsolete(id)); } - private async postUninstallExtension(extension: ILocalExtension, error?: any): TPromise { + private async postUninstallExtension(extension: ILocalExtension, error?: ErrorCode): TPromise { if (!error) { await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall); } From 358641a473476ab237fb460572d87921fb699932 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 19 Sep 2017 15:12:51 -0700 Subject: [PATCH 55/67] Run --export-default-configuration with fresh user-data-dir and extensions-dir --- build/gulpfile.vscode.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 3c351d454ad..81dd6d2a7a8 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -479,8 +479,10 @@ gulp.task('generate-vscode-configuration', () => { return reject(new Error('$AGENT_BUILDDIRECTORY not set')); } + const userDataDir = path.join(os.tmpdir(), 'tmpuserdata'); + const extensionsDir = path.join(os.tmpdir(), 'tmpextdir'); const appPath = path.join(buildDir, 'VSCode-darwin/Visual\\ Studio\\ Code\\ -\\ Insiders.app/Contents/Resources/app/bin/code'); - const codeProc = cp.exec(`${appPath} --export-default-configuration='${allConfigDetailsPath}' --wait`); + const codeProc = cp.exec(`${appPath} --export-default-configuration='${allConfigDetailsPath}' --wait --user-data-dir='${userDataDir}' --extensions-dir='${extensionsDir}'`); const timer = setTimeout(() => { codeProc.kill(); From 6d6d83b2f542c4618aa6ff535a67864c54177a7c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 19 Sep 2017 16:35:52 -0700 Subject: [PATCH 56/67] Pick up latest TS 2.5.3 insiders Fixes #33972 --- extensions/npm-shrinkwrap.json | 6 +++--- extensions/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/npm-shrinkwrap.json b/extensions/npm-shrinkwrap.json index 2cdcc763dd5..cf47f857cf2 100644 --- a/extensions/npm-shrinkwrap.json +++ b/extensions/npm-shrinkwrap.json @@ -3,9 +3,9 @@ "version": "0.0.1", "dependencies": { "typescript": { - "version": "2.5.3-insiders.20170909", - "from": "typescript@2.5.3-insiders.20170909", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3-insiders.20170909.tgz" + "version": "2.5.3-insiders.20170919", + "from": "typescript@2.5.3-insiders.20170919", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3-insiders.20170919.tgz" } } } diff --git a/extensions/package.json b/extensions/package.json index 3ad8d484a3d..9e0ef9f6fd2 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "2.5.3-insiders.20170909" + "typescript": "2.5.3-insiders.20170919" }, "scripts": { "postinstall": "node ./postinstall" From ce3c4b3ca0b8ce7ca97bae058a243ca8dc8f5f4f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 09:08:02 +0900 Subject: [PATCH 57/67] Uplevel xterm.js Fixes #34493 Fixes #34563 --- npm-shrinkwrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ad603585240..35551d36615 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -574,7 +574,7 @@ "xterm": { "version": "3.0.0", "from": "Tyriar/xterm.js#vscode-release/1.17", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#35088059e61ba654ac78df453633c7a9272ed8bd" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#851dff7253fc9718bb90f691fc79a7c3c542bcfa" }, "yauzl": { "version": "2.8.0", From 474a29ee48f39b31288268c933a03825ed62213e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 19 Sep 2017 17:59:08 -0700 Subject: [PATCH 58/67] Replace duplicated textSpan conversion logic --- .../src/features/baseCodeLensProvider.ts | 6 ++---- .../typescript/src/features/codeActionProvider.ts | 5 ++--- .../src/features/completionItemProvider.ts | 3 ++- .../src/features/definitionProviderBase.ts | 5 +++-- .../src/features/documentHighlightProvider.ts | 9 +++++---- .../src/features/documentSymbolProvider.ts | 10 ++++------ .../typescript/src/features/formattingProvider.ts | 4 ++-- .../typescript/src/features/hoverProvider.ts | 5 +++-- .../features/implementationsCodeLensProvider.ts | 5 ++--- .../typescript/src/features/refactorProvider.ts | 5 ++--- .../typescript/src/features/referenceProvider.ts | 7 +++---- .../src/features/referencesCodeLensProvider.ts | 6 ++---- .../typescript/src/features/renameProvider.ts | 7 +++---- .../src/features/workspaceSymbolProvider.ts | 5 +++-- extensions/typescript/src/utils/convert.ts | 14 ++++++++++++++ 15 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 extensions/typescript/src/utils/convert.ts diff --git a/extensions/typescript/src/features/baseCodeLensProvider.ts b/extensions/typescript/src/features/baseCodeLensProvider.ts index 84ba69a0a96..cb8e675db4d 100644 --- a/extensions/typescript/src/features/baseCodeLensProvider.ts +++ b/extensions/typescript/src/features/baseCodeLensProvider.ts @@ -7,6 +7,7 @@ import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export class ReferencesCodeLens extends CodeLens { constructor( @@ -99,10 +100,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider return null; } - const range = new Range( - span.start.line - 1, span.start.offset - 1, - span.end.line - 1, span.end.offset - 1); - + const range = textSpanToRange(span); const text = document.getText(range); const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}(\\b|\\W)`, 'gm'); diff --git a/extensions/typescript/src/features/codeActionProvider.ts b/extensions/typescript/src/features/codeActionProvider.ts index c48caaf79e4..925eac1fff4 100644 --- a/extensions/typescript/src/features/codeActionProvider.ts +++ b/extensions/typescript/src/features/codeActionProvider.ts @@ -7,6 +7,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; interface NumberSet { [key: number]: boolean; @@ -112,9 +113,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider for (const change of action.changes) { for (const textChange of change.textChanges) { workspaceEdit.replace(this.client.asUrl(change.fileName), - new Range( - textChange.start.line - 1, textChange.start.offset - 1, - textChange.end.line - 1, textChange.end.offset - 1), + textSpanToRange(textChange), textChange.newText); } } diff --git a/extensions/typescript/src/features/completionItemProvider.ts b/extensions/typescript/src/features/completionItemProvider.ts index 1fbcb707863..759642b929d 100644 --- a/extensions/typescript/src/features/completionItemProvider.ts +++ b/extensions/typescript/src/features/completionItemProvider.ts @@ -11,6 +11,7 @@ import TypingsStatus from '../utils/typingsStatus'; import * as PConst from '../protocol.const'; import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails, FileLocationRequestArgs } from '../protocol'; import * as Previewer from './previewer'; +import { textSpanToRange } from '../utils/convert'; import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); @@ -32,7 +33,7 @@ class MyCompletionItem extends CompletionItem { let span: protocol.TextSpan = entry.replacementSpan; // The indexing for the range returned by the server uses 1-based indexing. // We convert to 0-based indexing. - this.textEdit = TextEdit.replace(new Range(span.start.line - 1, span.start.offset - 1, span.end.line - 1, span.end.offset - 1), entry.name); + this.textEdit = TextEdit.replace(textSpanToRange(span), entry.name); } else { // Try getting longer, prefix based range for completions that span words const wordRange = document.getWordRangeAtPosition(position); diff --git a/extensions/typescript/src/features/definitionProviderBase.ts b/extensions/typescript/src/features/definitionProviderBase.ts index 78768df2fee..e137a1e3de7 100644 --- a/extensions/typescript/src/features/definitionProviderBase.ts +++ b/extensions/typescript/src/features/definitionProviderBase.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Position, Range, CancellationToken, Location } from 'vscode'; +import { TextDocument, Position, CancellationToken, Location } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptDefinitionProviderBase { constructor( @@ -37,7 +38,7 @@ export default class TypeScriptDefinitionProviderBase { if (resource === null) { return null; } else { - return new Location(resource, new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1)); + return new Location(resource, textSpanToRange(location)); } }).filter(x => x !== null) as Location[]; }, () => { diff --git a/extensions/typescript/src/features/documentHighlightProvider.ts b/extensions/typescript/src/features/documentHighlightProvider.ts index 15224ca2c2e..97733afb1ce 100644 --- a/extensions/typescript/src/features/documentHighlightProvider.ts +++ b/extensions/typescript/src/features/documentHighlightProvider.ts @@ -7,6 +7,7 @@ import { DocumentHighlightProvider, DocumentHighlight, DocumentHighlightKind, Te import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider { @@ -37,10 +38,10 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh return []; } } - return data.map((item) => { - return new DocumentHighlight(new Range(item.start.line - 1, item.start.offset - 1, item.end.line - 1, item.end.offset - 1), - item.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read); - }); + return data.map(item => + new DocumentHighlight( + textSpanToRange(item), + item.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read)); } return []; }, () => { diff --git a/extensions/typescript/src/features/documentSymbolProvider.ts b/extensions/typescript/src/features/documentSymbolProvider.ts index 456776cf3a6..57f507840d8 100644 --- a/extensions/typescript/src/features/documentSymbolProvider.ts +++ b/extensions/typescript/src/features/documentSymbolProvider.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, Range, Location, CancellationToken, Uri } from 'vscode'; +import { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, Location, CancellationToken, Uri } from 'vscode'; import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; const outlineTypeTable: { [kind: string]: SymbolKind } = Object.create(null); outlineTypeTable[PConst.Kind.module] = SymbolKind.Module; @@ -25,9 +26,6 @@ outlineTypeTable[PConst.Kind.variable] = SymbolKind.Variable; outlineTypeTable[PConst.Kind.function] = SymbolKind.Function; outlineTypeTable[PConst.Kind.localFunction] = SymbolKind.Function; -function textSpan2Range(value: Proto.TextSpan): Range { - return new Range(value.start.line - 1, value.start.offset - 1, value.end.line - 1, value.end.offset - 1); -} export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolProvider { public constructor( @@ -73,7 +71,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP let result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpan2Range(item.spans[0]))); + new Location(resource, textSpanToRange(item.spans[0]))); foldingMap[key] = result; bucket.push(result); } @@ -88,7 +86,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP const result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpan2Range(item.spans[0])) + new Location(resource, textSpanToRange(item.spans[0])) ); if (item.childItems && item.childItems.length > 0) { for (const child of item.childItems) { diff --git a/extensions/typescript/src/features/formattingProvider.ts b/extensions/typescript/src/features/formattingProvider.ts index 1c5e7866b8f..10e20dd9b39 100644 --- a/extensions/typescript/src/features/formattingProvider.ts +++ b/extensions/typescript/src/features/formattingProvider.ts @@ -7,6 +7,7 @@ import { workspace as Workspace, DocumentRangeFormattingEditProvider, OnTypeForm import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; interface Configuration { enable: boolean; @@ -205,8 +206,7 @@ export class TypeScriptFormattingProvider implements DocumentRangeFormattingEdit } private codeEdit2SingleEditOperation(edit: Proto.CodeEdit): TextEdit { - return new TextEdit(new Range(edit.start.line - 1, edit.start.offset - 1, edit.end.line - 1, edit.end.offset - 1), - edit.newText); + return new TextEdit(textSpanToRange(edit), edit.newText); } private getFormatOptions(options: FormattingOptions): Proto.FormatCodeSettings { diff --git a/extensions/typescript/src/features/hoverProvider.ts b/extensions/typescript/src/features/hoverProvider.ts index 9fd1483b625..89971ba5b61 100644 --- a/extensions/typescript/src/features/hoverProvider.ts +++ b/extensions/typescript/src/features/hoverProvider.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { HoverProvider, Hover, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { HoverProvider, Hover, TextDocument, Position, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { tagsMarkdownPreview } from './previewer'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptHoverProvider implements HoverProvider { @@ -31,7 +32,7 @@ export default class TypeScriptHoverProvider implements HoverProvider { const data = response.body; return new Hover( TypeScriptHoverProvider.getContents(data), - new Range(data.start.line - 1, data.start.offset - 1, data.end.line - 1, data.end.offset - 1)); + textSpanToRange(data)); } } catch (e) { // noop diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts index 30daa644a4e..9a6d1a79b42 100644 --- a/extensions/typescript/src/features/implementationsCodeLensProvider.ts +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -9,6 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -50,9 +51,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip // Only take first line on implementation: https://github.com/Microsoft/vscode/issues/23924 new Location(this.client.asUrl(reference.file), reference.start.line === reference.end.line - ? new Range( - reference.start.line - 1, reference.start.offset - 1, - reference.end.line - 1, reference.end.offset - 1) + ? textSpanToRange(reference) : new Range( reference.start.line - 1, reference.start.offset - 1, reference.start.line, 0))) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 1b9062998b5..563160640c9 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -9,6 +9,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptRefactorProvider implements CodeActionProvider { @@ -85,9 +86,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { for (const edit of edits) { for (const textChange of edit.textChanges) { workspaceEdit.replace(this.client.asUrl(edit.fileName), - new Range( - textChange.start.line - 1, textChange.start.offset - 1, - textChange.end.line - 1, textChange.end.offset - 1), + textSpanToRange(textChange), textChange.newText); } } diff --git a/extensions/typescript/src/features/referenceProvider.ts b/extensions/typescript/src/features/referenceProvider.ts index 9f7ccf9f49a..b6af1f23033 100644 --- a/extensions/typescript/src/features/referenceProvider.ts +++ b/extensions/typescript/src/features/referenceProvider.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ReferenceProvider, Location, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { ReferenceProvider, Location, TextDocument, Position, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptReferenceSupport implements ReferenceProvider { public constructor( @@ -35,9 +36,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { continue; } const url = this.client.asUrl(ref.file); - const location = new Location( - url, - new Range(ref.start.line - 1, ref.start.offset - 1, ref.end.line - 1, ref.end.offset - 1)); + const location = new Location(url, textSpanToRange(ref)); result.push(location); } return result; diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 5576bfc4825..40c56f48df9 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -9,6 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -47,10 +48,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase const locations = response.body.refs .map(reference => - new Location(this.client.asUrl(reference.file), - new Range( - reference.start.line - 1, reference.start.offset - 1, - reference.end.line - 1, reference.end.offset - 1))) + new Location(this.client.asUrl(reference.file), textSpanToRange(reference))) .filter(location => // Exclude original definition from references !(location.uri.fsPath === codeLens.document.fsPath && diff --git a/extensions/typescript/src/features/renameProvider.ts b/extensions/typescript/src/features/renameProvider.ts index 291be640ca7..e7adfc1cea7 100644 --- a/extensions/typescript/src/features/renameProvider.ts +++ b/extensions/typescript/src/features/renameProvider.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RenameProvider, WorkspaceEdit, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { RenameProvider, WorkspaceEdit, TextDocument, Position, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; export default class TypeScriptRenameProvider implements RenameProvider { public constructor( @@ -49,9 +50,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { continue; } for (const textSpan of spanGroup.locs) { - result.replace(resource, - new Range(textSpan.start.line - 1, textSpan.start.offset - 1, textSpan.end.line - 1, textSpan.end.offset - 1), - newName); + result.replace(resource, textSpanToRange(textSpan), newName); } } return result; diff --git a/extensions/typescript/src/features/workspaceSymbolProvider.ts b/extensions/typescript/src/features/workspaceSymbolProvider.ts index ab2329501a3..6ebc45b36bb 100644 --- a/extensions/typescript/src/features/workspaceSymbolProvider.ts +++ b/extensions/typescript/src/features/workspaceSymbolProvider.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, SymbolKind, Range, Location, CancellationToken } from 'vscode'; +import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, SymbolKind, Location, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { textSpanToRange } from '../utils/convert'; function getSymbolKind(item: Proto.NavtoItem): SymbolKind { switch (item.kind) { @@ -67,7 +68,7 @@ export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbo if (!item.containerName && item.kind === 'alias') { continue; } - const range = new Range(item.start.line - 1, item.start.offset - 1, item.end.line - 1, item.end.offset - 1); + const range = textSpanToRange(item); let label = item.name; if (item.kind === 'method' || item.kind === 'function') { label += '()'; diff --git a/extensions/typescript/src/utils/convert.ts b/extensions/typescript/src/utils/convert.ts new file mode 100644 index 00000000000..92511d1fb1a --- /dev/null +++ b/extensions/typescript/src/utils/convert.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from 'vscode'; + +import * as Proto from '../protocol'; + + +export const textSpanToRange = (span: Proto.TextSpan) => + new Range( + span.start.line - 1, span.start.offset - 1, + span.end.line - 1, span.end.offset - 1); \ No newline at end of file From 703e7408d63e2bc1e65ce999fd078e33461717e0 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 19 Sep 2017 18:14:41 -0700 Subject: [PATCH 59/67] Continue moving range/position conversion to single location --- .../src/features/codeActionProvider.ts | 8 ++----- .../src/features/completionItemProvider.ts | 17 ++++----------- .../src/features/definitionProviderBase.ts | 8 ++----- .../src/features/documentHighlightProvider.ts | 9 ++------ .../typescript/src/features/hoverProvider.ts | 9 ++------ .../implementationsCodeLensProvider.ts | 8 ++----- .../src/features/jsDocCompletionProvider.ts | 9 +++----- .../src/features/refactorProvider.ts | 19 ++++------------- .../src/features/referenceProvider.ts | 9 ++------ .../features/referencesCodeLensProvider.ts | 8 ++----- extensions/typescript/src/utils/convert.ts | 21 +++++++++++++++---- 11 files changed, 42 insertions(+), 83 deletions(-) diff --git a/extensions/typescript/src/features/codeActionProvider.ts b/extensions/typescript/src/features/codeActionProvider.ts index 925eac1fff4..76051c6e505 100644 --- a/extensions/typescript/src/features/codeActionProvider.ts +++ b/extensions/typescript/src/features/codeActionProvider.ts @@ -7,7 +7,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, rangeToFileRange } from '../utils/convert'; interface NumberSet { [key: number]: boolean; @@ -68,11 +68,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider formattingOptions: formattingOptions }; const args: Proto.CodeFixRequestArgs = { - file: file, - startLine: range.start.line + 1, - endLine: range.end.line + 1, - startOffset: range.start.character + 1, - endOffset: range.end.character + 1, + ...rangeToFileRange(file, range), errorCodes: Array.from(supportedActions) }; const response = await this.client.execute('getCodeFixes', args, token); diff --git a/extensions/typescript/src/features/completionItemProvider.ts b/extensions/typescript/src/features/completionItemProvider.ts index 759642b929d..b225fa375c0 100644 --- a/extensions/typescript/src/features/completionItemProvider.ts +++ b/extensions/typescript/src/features/completionItemProvider.ts @@ -9,9 +9,9 @@ import { ITypescriptServiceClient } from '../typescriptService'; import TypingsStatus from '../utils/typingsStatus'; import * as PConst from '../protocol.const'; -import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails, FileLocationRequestArgs } from '../protocol'; +import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails } from '../protocol'; import * as Previewer from './previewer'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); @@ -175,12 +175,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP if (!file) { return Promise.resolve([]); } - const args: CompletionsRequestArgs = { - file: file, - line: position.line + 1, - offset: position.character + 1 - }; - + const args: CompletionsRequestArgs = positionToFileLocation(file, position); return this.client.execute('completions', args, token).then((msg) => { // This info has to come from the tsserver. See https://github.com/Microsoft/TypeScript/issues/2831 // let isMemberCompletion = false; @@ -270,11 +265,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } private isValidFunctionCompletionContext(filepath: string, position: Position): Promise { - const args: FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = positionToFileLocation(filepath, position); // Workaround for https://github.com/Microsoft/TypeScript/issues/12677 // Don't complete function calls inside of destructive assigments or imports return this.client.execute('quickinfo', args).then(infoResponse => { diff --git a/extensions/typescript/src/features/definitionProviderBase.ts b/extensions/typescript/src/features/definitionProviderBase.ts index e137a1e3de7..7f241eadc6a 100644 --- a/extensions/typescript/src/features/definitionProviderBase.ts +++ b/extensions/typescript/src/features/definitionProviderBase.ts @@ -7,7 +7,7 @@ import { TextDocument, Position, CancellationToken, Location } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; export default class TypeScriptDefinitionProviderBase { constructor( @@ -23,11 +23,7 @@ export default class TypeScriptDefinitionProviderBase { if (!filepath) { return Promise.resolve(null); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = positionToFileLocation(filepath, position); return this.client.execute(definitionType, args, token).then(response => { const locations: Proto.FileSpan[] = (response && response.body) || []; if (!locations || locations.length === 0) { diff --git a/extensions/typescript/src/features/documentHighlightProvider.ts b/extensions/typescript/src/features/documentHighlightProvider.ts index 97733afb1ce..283a88ef880 100644 --- a/extensions/typescript/src/features/documentHighlightProvider.ts +++ b/extensions/typescript/src/features/documentHighlightProvider.ts @@ -5,9 +5,8 @@ import { DocumentHighlightProvider, DocumentHighlight, DocumentHighlightKind, TextDocument, Position, Range, CancellationToken } from 'vscode'; -import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider { @@ -19,11 +18,7 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh if (!filepath) { return Promise.resolve([]); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = positionToFileLocation(filepath, position); return this.client.execute('occurrences', args, token).then((response): DocumentHighlight[] => { let data = response.body; if (data && data.length) { diff --git a/extensions/typescript/src/features/hoverProvider.ts b/extensions/typescript/src/features/hoverProvider.ts index 89971ba5b61..693e990fe25 100644 --- a/extensions/typescript/src/features/hoverProvider.ts +++ b/extensions/typescript/src/features/hoverProvider.ts @@ -8,7 +8,7 @@ import { HoverProvider, Hover, TextDocument, Position, CancellationToken } from import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { tagsMarkdownPreview } from './previewer'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; export default class TypeScriptHoverProvider implements HoverProvider { @@ -20,12 +20,7 @@ export default class TypeScriptHoverProvider implements HoverProvider { if (!filepath) { return undefined; } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; - + const args = positionToFileLocation(filepath, position); try { const response = await this.client.execute('quickinfo', args, token); if (response && response.body) { diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts index 9a6d1a79b42..7890d665084 100644 --- a/extensions/typescript/src/features/implementationsCodeLensProvider.ts +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -9,7 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -36,11 +36,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args: Proto.FileLocationRequestArgs = { - file: codeLens.file, - line: codeLens.range.start.line + 1, - offset: codeLens.range.start.character + 1 - }; + const args = positionToFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('implementation', args, token).then(response => { if (!response || !response.body) { throw codeLens; diff --git a/extensions/typescript/src/features/jsDocCompletionProvider.ts b/extensions/typescript/src/features/jsDocCompletionProvider.ts index 50f0147cbc0..c78116fb09e 100644 --- a/extensions/typescript/src/features/jsDocCompletionProvider.ts +++ b/extensions/typescript/src/features/jsDocCompletionProvider.ts @@ -6,9 +6,10 @@ import { Position, Range, CompletionItemProvider, CompletionItemKind, TextDocument, CancellationToken, CompletionItem, window, Uri, ProviderResult, TextEditor, SnippetString, workspace } from 'vscode'; import { ITypescriptServiceClient } from '../typescriptService'; -import { FileLocationRequestArgs, DocCommandTemplateResponse } from '../protocol'; +import { DocCommandTemplateResponse } from '../protocol'; import * as nls from 'vscode-nls'; +import { positionToFileLocation } from '../utils/convert'; const localize = nls.loadMessageBundle(); const configurationNamespace = 'jsDocCompletion'; @@ -118,11 +119,7 @@ export class TryCompleteJsDocCommand { } private tryInsertJsDocFromTemplate(editor: TextEditor, file: string, position: Position): Promise { - const args: FileLocationRequestArgs = { - file: file, - line: position.line + 1, - offset: position.character + 1 - }; + const args = positionToFileLocation(file, position); return Promise.race([ this.lazyClient().execute('docCommentTemplate', args), new Promise((_, reject) => setTimeout(reject, 250)) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 563160640c9..a67ce08f2c7 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -9,7 +9,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, rangeToFileRange } from '../utils/convert'; export default class TypeScriptRefactorProvider implements CodeActionProvider { @@ -43,14 +43,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return []; } - const args: Proto.GetApplicableRefactorsRequestArgs = { - file: file, - startLine: range.start.line + 1, - startOffset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 - }; - + const args: Proto.GetApplicableRefactorsRequestArgs = rangeToFileRange(file, range); try { const response = await this.client.execute('getApplicableRefactors', args, token); if (!response || !response.body) { @@ -107,13 +100,9 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { private async doRefactoring(file: string, refactor: string, action: string, range: Range): Promise { const args: Proto.GetEditsForRefactorRequestArgs = { - file, + ...rangeToFileRange(file, range), refactor, - action, - startLine: range.start.line + 1, - startOffset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 + action }; const response = await this.client.execute('getEditsForRefactor', args); diff --git a/extensions/typescript/src/features/referenceProvider.ts b/extensions/typescript/src/features/referenceProvider.ts index b6af1f23033..4e4847b1106 100644 --- a/extensions/typescript/src/features/referenceProvider.ts +++ b/extensions/typescript/src/features/referenceProvider.ts @@ -5,9 +5,8 @@ import { ReferenceProvider, Location, TextDocument, Position, CancellationToken } from 'vscode'; -import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; export default class TypeScriptReferenceSupport implements ReferenceProvider { public constructor( @@ -18,11 +17,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { if (!filepath) { return Promise.resolve([]); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = positionToFileLocation(filepath, position); const apiVersion = this.client.apiVersion; return this.client.execute('references', args, token).then((msg) => { const result: Location[] = []; diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 40c56f48df9..11cd28b7ef7 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -9,7 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { textSpanToRange, positionToFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -36,11 +36,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args: Proto.FileLocationRequestArgs = { - file: codeLens.file, - line: codeLens.range.start.line + 1, - offset: codeLens.range.start.character + 1 - }; + const args = positionToFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('references', args, token).then(response => { if (!response || !response.body) { throw codeLens; diff --git a/extensions/typescript/src/utils/convert.ts b/extensions/typescript/src/utils/convert.ts index 92511d1fb1a..40e2dbef7b1 100644 --- a/extensions/typescript/src/utils/convert.ts +++ b/extensions/typescript/src/utils/convert.ts @@ -3,12 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Range } from 'vscode'; - +import * as vscode from 'vscode'; import * as Proto from '../protocol'; export const textSpanToRange = (span: Proto.TextSpan) => - new Range( + new vscode.Range( span.start.line - 1, span.start.offset - 1, - span.end.line - 1, span.end.offset - 1); \ No newline at end of file + span.end.line - 1, span.end.offset - 1); + +export const positionToFileLocation = (file: string, position: vscode.Position): Proto.FileLocationRequestArgs => ({ + file, + line: position.line + 1, + offset: position.character + 1 +}); + +export const rangeToFileRange = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ + file, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 +}); \ No newline at end of file From f1043f2b9d786a6ae4579f7d564c8ee384c0f360 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 19 Sep 2017 18:25:20 -0700 Subject: [PATCH 60/67] Continue moving vs loc to ts loc location into single set of functions --- .../typescript/src/features/baseCodeLensProvider.ts | 4 ++-- .../typescript/src/features/codeActionProvider.ts | 6 +++--- .../src/features/completionItemProvider.ts | 12 +++++------- .../src/features/definitionProviderBase.ts | 6 +++--- .../src/features/documentHighlightProvider.ts | 6 +++--- .../src/features/documentSymbolProvider.ts | 6 +++--- .../typescript/src/features/formattingProvider.ts | 4 ++-- extensions/typescript/src/features/hoverProvider.ts | 6 +++--- .../src/features/implementationsCodeLensProvider.ts | 6 +++--- .../src/features/jsDocCompletionProvider.ts | 4 ++-- .../typescript/src/features/refactorProvider.ts | 13 ++++++------- .../typescript/src/features/referenceProvider.ts | 6 +++--- .../src/features/referencesCodeLensProvider.ts | 6 +++--- .../typescript/src/features/renameProvider.ts | 8 +++----- .../src/features/signatureHelpProvider.ts | 7 ++----- .../src/features/workspaceSymbolProvider.ts | 4 ++-- extensions/typescript/src/typescriptMain.ts | 3 ++- extensions/typescript/src/utils/convert.ts | 9 ++++++--- 18 files changed, 56 insertions(+), 60 deletions(-) diff --git a/extensions/typescript/src/features/baseCodeLensProvider.ts b/extensions/typescript/src/features/baseCodeLensProvider.ts index cb8e675db4d..9f5d6012d80 100644 --- a/extensions/typescript/src/features/baseCodeLensProvider.ts +++ b/extensions/typescript/src/features/baseCodeLensProvider.ts @@ -7,7 +7,7 @@ import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { tsTextSpanToVsRange } from '../utils/convert'; export class ReferencesCodeLens extends CodeLens { constructor( @@ -100,7 +100,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider return null; } - const range = textSpanToRange(span); + const range = tsTextSpanToVsRange(span); const text = document.getText(range); const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}(\\b|\\W)`, 'gm'); diff --git a/extensions/typescript/src/features/codeActionProvider.ts b/extensions/typescript/src/features/codeActionProvider.ts index 76051c6e505..fefe85a3835 100644 --- a/extensions/typescript/src/features/codeActionProvider.ts +++ b/extensions/typescript/src/features/codeActionProvider.ts @@ -7,7 +7,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, rangeToFileRange } from '../utils/convert'; +import { tsTextSpanToVsRange, vsRangeToTsFileRange } from '../utils/convert'; interface NumberSet { [key: number]: boolean; @@ -68,7 +68,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider formattingOptions: formattingOptions }; const args: Proto.CodeFixRequestArgs = { - ...rangeToFileRange(file, range), + ...vsRangeToTsFileRange(file, range), errorCodes: Array.from(supportedActions) }; const response = await this.client.execute('getCodeFixes', args, token); @@ -109,7 +109,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider for (const change of action.changes) { for (const textChange of change.textChanges) { workspaceEdit.replace(this.client.asUrl(change.fileName), - textSpanToRange(textChange), + tsTextSpanToVsRange(textChange), textChange.newText); } } diff --git a/extensions/typescript/src/features/completionItemProvider.ts b/extensions/typescript/src/features/completionItemProvider.ts index b225fa375c0..e91e71ac9fd 100644 --- a/extensions/typescript/src/features/completionItemProvider.ts +++ b/extensions/typescript/src/features/completionItemProvider.ts @@ -11,7 +11,7 @@ import TypingsStatus from '../utils/typingsStatus'; import * as PConst from '../protocol.const'; import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails } from '../protocol'; import * as Previewer from './previewer'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); @@ -33,7 +33,7 @@ class MyCompletionItem extends CompletionItem { let span: protocol.TextSpan = entry.replacementSpan; // The indexing for the range returned by the server uses 1-based indexing. // We convert to 0-based indexing. - this.textEdit = TextEdit.replace(textSpanToRange(span), entry.name); + this.textEdit = TextEdit.replace(tsTextSpanToVsRange(span), entry.name); } else { // Try getting longer, prefix based range for completions that span words const wordRange = document.getWordRangeAtPosition(position); @@ -175,7 +175,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP if (!file) { return Promise.resolve([]); } - const args: CompletionsRequestArgs = positionToFileLocation(file, position); + const args: CompletionsRequestArgs = vsPositionToTsFileLocation(file, position); return this.client.execute('completions', args, token).then((msg) => { // This info has to come from the tsserver. See https://github.com/Microsoft/TypeScript/issues/2831 // let isMemberCompletion = false; @@ -234,9 +234,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP return null; } const args: CompletionDetailsRequestArgs = { - file: filepath, - line: item.position.line + 1, - offset: item.position.character + 1, + ...vsPositionToTsFileLocation(filepath, item.position), entryNames: [item.label] }; return this.client.execute('completionEntryDetails', args, token).then((response) => { @@ -265,7 +263,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } private isValidFunctionCompletionContext(filepath: string, position: Position): Promise { - const args = positionToFileLocation(filepath, position); + const args = vsPositionToTsFileLocation(filepath, position); // Workaround for https://github.com/Microsoft/TypeScript/issues/12677 // Don't complete function calls inside of destructive assigments or imports return this.client.execute('quickinfo', args).then(infoResponse => { diff --git a/extensions/typescript/src/features/definitionProviderBase.ts b/extensions/typescript/src/features/definitionProviderBase.ts index 7f241eadc6a..bf5ffabd704 100644 --- a/extensions/typescript/src/features/definitionProviderBase.ts +++ b/extensions/typescript/src/features/definitionProviderBase.ts @@ -7,7 +7,7 @@ import { TextDocument, Position, CancellationToken, Location } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptDefinitionProviderBase { constructor( @@ -23,7 +23,7 @@ export default class TypeScriptDefinitionProviderBase { if (!filepath) { return Promise.resolve(null); } - const args = positionToFileLocation(filepath, position); + const args = vsPositionToTsFileLocation(filepath, position); return this.client.execute(definitionType, args, token).then(response => { const locations: Proto.FileSpan[] = (response && response.body) || []; if (!locations || locations.length === 0) { @@ -34,7 +34,7 @@ export default class TypeScriptDefinitionProviderBase { if (resource === null) { return null; } else { - return new Location(resource, textSpanToRange(location)); + return new Location(resource, tsTextSpanToVsRange(location)); } }).filter(x => x !== null) as Location[]; }, () => { diff --git a/extensions/typescript/src/features/documentHighlightProvider.ts b/extensions/typescript/src/features/documentHighlightProvider.ts index 283a88ef880..d91cb8fd90b 100644 --- a/extensions/typescript/src/features/documentHighlightProvider.ts +++ b/extensions/typescript/src/features/documentHighlightProvider.ts @@ -6,7 +6,7 @@ import { DocumentHighlightProvider, DocumentHighlight, DocumentHighlightKind, TextDocument, Position, Range, CancellationToken } from 'vscode'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider { @@ -18,7 +18,7 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh if (!filepath) { return Promise.resolve([]); } - const args = positionToFileLocation(filepath, position); + const args = vsPositionToTsFileLocation(filepath, position); return this.client.execute('occurrences', args, token).then((response): DocumentHighlight[] => { let data = response.body; if (data && data.length) { @@ -35,7 +35,7 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh } return data.map(item => new DocumentHighlight( - textSpanToRange(item), + tsTextSpanToVsRange(item), item.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read)); } return []; diff --git a/extensions/typescript/src/features/documentSymbolProvider.ts b/extensions/typescript/src/features/documentSymbolProvider.ts index 57f507840d8..bf289d24ee2 100644 --- a/extensions/typescript/src/features/documentSymbolProvider.ts +++ b/extensions/typescript/src/features/documentSymbolProvider.ts @@ -8,7 +8,7 @@ import { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, Lo import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { tsTextSpanToVsRange } from '../utils/convert'; const outlineTypeTable: { [kind: string]: SymbolKind } = Object.create(null); outlineTypeTable[PConst.Kind.module] = SymbolKind.Module; @@ -71,7 +71,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP let result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpanToRange(item.spans[0]))); + new Location(resource, tsTextSpanToVsRange(item.spans[0]))); foldingMap[key] = result; bucket.push(result); } @@ -86,7 +86,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP const result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpanToRange(item.spans[0])) + new Location(resource, tsTextSpanToVsRange(item.spans[0])) ); if (item.childItems && item.childItems.length > 0) { for (const child of item.childItems) { diff --git a/extensions/typescript/src/features/formattingProvider.ts b/extensions/typescript/src/features/formattingProvider.ts index 10e20dd9b39..b4f26fdcd0a 100644 --- a/extensions/typescript/src/features/formattingProvider.ts +++ b/extensions/typescript/src/features/formattingProvider.ts @@ -7,7 +7,7 @@ import { workspace as Workspace, DocumentRangeFormattingEditProvider, OnTypeForm import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { tsTextSpanToVsRange } from '../utils/convert'; interface Configuration { enable: boolean; @@ -206,7 +206,7 @@ export class TypeScriptFormattingProvider implements DocumentRangeFormattingEdit } private codeEdit2SingleEditOperation(edit: Proto.CodeEdit): TextEdit { - return new TextEdit(textSpanToRange(edit), edit.newText); + return new TextEdit(tsTextSpanToVsRange(edit), edit.newText); } private getFormatOptions(options: FormattingOptions): Proto.FormatCodeSettings { diff --git a/extensions/typescript/src/features/hoverProvider.ts b/extensions/typescript/src/features/hoverProvider.ts index 693e990fe25..9ee37c5954b 100644 --- a/extensions/typescript/src/features/hoverProvider.ts +++ b/extensions/typescript/src/features/hoverProvider.ts @@ -8,7 +8,7 @@ import { HoverProvider, Hover, TextDocument, Position, CancellationToken } from import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { tagsMarkdownPreview } from './previewer'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptHoverProvider implements HoverProvider { @@ -20,14 +20,14 @@ export default class TypeScriptHoverProvider implements HoverProvider { if (!filepath) { return undefined; } - const args = positionToFileLocation(filepath, position); + const args = vsPositionToTsFileLocation(filepath, position); try { const response = await this.client.execute('quickinfo', args, token); if (response && response.body) { const data = response.body; return new Hover( TypeScriptHoverProvider.getContents(data), - textSpanToRange(data)); + tsTextSpanToVsRange(data)); } } catch (e) { // noop diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts index 7890d665084..a36ab6b2527 100644 --- a/extensions/typescript/src/features/implementationsCodeLensProvider.ts +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -9,7 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -36,7 +36,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args = positionToFileLocation(codeLens.file, codeLens.range.start); + const args = vsPositionToTsFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('implementation', args, token).then(response => { if (!response || !response.body) { throw codeLens; @@ -47,7 +47,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip // Only take first line on implementation: https://github.com/Microsoft/vscode/issues/23924 new Location(this.client.asUrl(reference.file), reference.start.line === reference.end.line - ? textSpanToRange(reference) + ? tsTextSpanToVsRange(reference) : new Range( reference.start.line - 1, reference.start.offset - 1, reference.start.line, 0))) diff --git a/extensions/typescript/src/features/jsDocCompletionProvider.ts b/extensions/typescript/src/features/jsDocCompletionProvider.ts index c78116fb09e..d9c87da1cc0 100644 --- a/extensions/typescript/src/features/jsDocCompletionProvider.ts +++ b/extensions/typescript/src/features/jsDocCompletionProvider.ts @@ -9,7 +9,7 @@ import { ITypescriptServiceClient } from '../typescriptService'; import { DocCommandTemplateResponse } from '../protocol'; import * as nls from 'vscode-nls'; -import { positionToFileLocation } from '../utils/convert'; +import { vsPositionToTsFileLocation } from '../utils/convert'; const localize = nls.loadMessageBundle(); const configurationNamespace = 'jsDocCompletion'; @@ -119,7 +119,7 @@ export class TryCompleteJsDocCommand { } private tryInsertJsDocFromTemplate(editor: TextEditor, file: string, position: Position): Promise { - const args = positionToFileLocation(file, position); + const args = vsPositionToTsFileLocation(file, position); return Promise.race([ this.lazyClient().execute('docCommentTemplate', args), new Promise((_, reject) => setTimeout(reject, 250)) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index a67ce08f2c7..d87d6c01076 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -5,11 +5,11 @@ 'use strict'; -import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit, window, QuickPickItem, Selection, Position } from 'vscode'; +import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit, window, QuickPickItem, Selection } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, rangeToFileRange } from '../utils/convert'; +import { tsTextSpanToVsRange, vsRangeToTsFileRange, tsLocationToVsPosition } from '../utils/convert'; export default class TypeScriptRefactorProvider implements CodeActionProvider { @@ -25,7 +25,6 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { commands.registerCommand(this.doRefactorCommandId, this.doRefactoring, this); commands.registerCommand(this.selectRefactorCommandId, this.selectRefactoring, this); - } public async provideCodeActions( @@ -43,7 +42,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return []; } - const args: Proto.GetApplicableRefactorsRequestArgs = rangeToFileRange(file, range); + const args: Proto.GetApplicableRefactorsRequestArgs = vsRangeToTsFileRange(file, range); try { const response = await this.client.execute('getApplicableRefactors', args, token); if (!response || !response.body) { @@ -79,7 +78,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { for (const edit of edits) { for (const textChange of edit.textChanges) { workspaceEdit.replace(this.client.asUrl(edit.fileName), - textSpanToRange(textChange), + tsTextSpanToVsRange(textChange), textChange.newText); } } @@ -100,7 +99,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { private async doRefactoring(file: string, refactor: string, action: string, range: Range): Promise { const args: Proto.GetEditsForRefactorRequestArgs = { - ...rangeToFileRange(file, range), + ...vsRangeToTsFileRange(file, range), refactor, action }; @@ -118,7 +117,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { const renameLocation = response.body.renameLocation; if (renameLocation) { if (window.activeTextEditor && window.activeTextEditor.document.uri.fsPath === file) { - const pos = new Position(renameLocation.line - 1, renameLocation.offset - 1); + const pos = tsLocationToVsPosition(renameLocation); window.activeTextEditor.selection = new Selection(pos, pos); await commands.executeCommand('editor.action.rename'); } diff --git a/extensions/typescript/src/features/referenceProvider.ts b/extensions/typescript/src/features/referenceProvider.ts index 4e4847b1106..1bce17a5b65 100644 --- a/extensions/typescript/src/features/referenceProvider.ts +++ b/extensions/typescript/src/features/referenceProvider.ts @@ -6,7 +6,7 @@ import { ReferenceProvider, Location, TextDocument, Position, CancellationToken } from 'vscode'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptReferenceSupport implements ReferenceProvider { public constructor( @@ -17,7 +17,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { if (!filepath) { return Promise.resolve([]); } - const args = positionToFileLocation(filepath, position); + const args = vsPositionToTsFileLocation(filepath, position); const apiVersion = this.client.apiVersion; return this.client.execute('references', args, token).then((msg) => { const result: Location[] = []; @@ -31,7 +31,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { continue; } const url = this.client.asUrl(ref.file); - const location = new Location(url, textSpanToRange(ref)); + const location = new Location(url, tsTextSpanToVsRange(ref)); result.push(location); } return result; diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 11cd28b7ef7..251337ab59c 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -9,7 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange, positionToFileLocation } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -36,7 +36,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args = positionToFileLocation(codeLens.file, codeLens.range.start); + const args = vsPositionToTsFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('references', args, token).then(response => { if (!response || !response.body) { throw codeLens; @@ -44,7 +44,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase const locations = response.body.refs .map(reference => - new Location(this.client.asUrl(reference.file), textSpanToRange(reference))) + new Location(this.client.asUrl(reference.file), tsTextSpanToVsRange(reference))) .filter(location => // Exclude original definition from references !(location.uri.fsPath === codeLens.document.fsPath && diff --git a/extensions/typescript/src/features/renameProvider.ts b/extensions/typescript/src/features/renameProvider.ts index e7adfc1cea7..26bbb6a29ea 100644 --- a/extensions/typescript/src/features/renameProvider.ts +++ b/extensions/typescript/src/features/renameProvider.ts @@ -7,7 +7,7 @@ import { RenameProvider, WorkspaceEdit, TextDocument, Position, CancellationToke import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptRenameProvider implements RenameProvider { public constructor( @@ -25,9 +25,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { } const args: Proto.RenameRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1, + ...vsPositionToTsFileLocation(filepath, position), findInStrings: false, findInComments: false }; @@ -50,7 +48,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { continue; } for (const textSpan of spanGroup.locs) { - result.replace(resource, textSpanToRange(textSpan), newName); + result.replace(resource, tsTextSpanToVsRange(textSpan), newName); } } return result; diff --git a/extensions/typescript/src/features/signatureHelpProvider.ts b/extensions/typescript/src/features/signatureHelpProvider.ts index d4fb33e2bfb..ea0de1f04d5 100644 --- a/extensions/typescript/src/features/signatureHelpProvider.ts +++ b/extensions/typescript/src/features/signatureHelpProvider.ts @@ -8,6 +8,7 @@ import { SignatureHelpProvider, SignatureHelp, SignatureInformation, ParameterIn import * as Previewer from './previewer'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptSignatureHelpProvider implements SignatureHelpProvider { @@ -19,11 +20,7 @@ export default class TypeScriptSignatureHelpProvider implements SignatureHelpPro if (!filepath) { return Promise.resolve(null); } - const args: Proto.SignatureHelpRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args: Proto.SignatureHelpRequestArgs = vsPositionToTsFileLocation(filepath, position); return this.client.execute('signatureHelp', args, token).then((response) => { const info = response.body; if (!info) { diff --git a/extensions/typescript/src/features/workspaceSymbolProvider.ts b/extensions/typescript/src/features/workspaceSymbolProvider.ts index 6ebc45b36bb..8885eb877ac 100644 --- a/extensions/typescript/src/features/workspaceSymbolProvider.ts +++ b/extensions/typescript/src/features/workspaceSymbolProvider.ts @@ -7,7 +7,7 @@ import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, Sym import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; -import { textSpanToRange } from '../utils/convert'; +import { tsTextSpanToVsRange } from '../utils/convert'; function getSymbolKind(item: Proto.NavtoItem): SymbolKind { switch (item.kind) { @@ -68,7 +68,7 @@ export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbo if (!item.containerName && item.kind === 'alias') { continue; } - const range = textSpanToRange(item); + const range = tsTextSpanToVsRange(item); let label = item.name; if (item.kind === 'method' || item.kind === 'function') { label += '()'; diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 9c472f187f0..87c312eb268 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -33,6 +33,7 @@ import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; import VersionStatus from './utils/versionStatus'; import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from './utils/plugins'; import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './utils/tsconfig'; +import { tsLocationToVsPosition } from './utils/convert'; interface LanguageDescription { id: string; @@ -681,7 +682,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost { const result: Diagnostic[] = []; for (let diagnostic of diagnostics) { const { start, end, text } = diagnostic; - const range = new Range(start.line - 1, start.offset - 1, end.line - 1, end.offset - 1); + const range = new Range(tsLocationToVsPosition(start), tsLocationToVsPosition(end)); const converted = new Diagnostic(range, text); converted.severity = this.getDiagnosticSeverity(diagnostic); converted.source = diagnostic.source || source; diff --git a/extensions/typescript/src/utils/convert.ts b/extensions/typescript/src/utils/convert.ts index 40e2dbef7b1..299597f34d5 100644 --- a/extensions/typescript/src/utils/convert.ts +++ b/extensions/typescript/src/utils/convert.ts @@ -7,18 +7,21 @@ import * as vscode from 'vscode'; import * as Proto from '../protocol'; -export const textSpanToRange = (span: Proto.TextSpan) => +export const tsTextSpanToVsRange = (span: Proto.TextSpan) => new vscode.Range( span.start.line - 1, span.start.offset - 1, span.end.line - 1, span.end.offset - 1); -export const positionToFileLocation = (file: string, position: vscode.Position): Proto.FileLocationRequestArgs => ({ +export const tsLocationToVsPosition = (tslocation: Proto.Location) => + new vscode.Position(tslocation.line - 1, tslocation.offset - 1); + +export const vsPositionToTsFileLocation = (file: string, position: vscode.Position): Proto.FileLocationRequestArgs => ({ file, line: position.line + 1, offset: position.character + 1 }); -export const rangeToFileRange = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ +export const vsRangeToTsFileRange = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ file, startLine: range.start.line + 1, startOffset: range.start.character + 1, From 54f23869cca42861e7ee7f3f454e6bd6d2a7d4c2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:01:20 +0900 Subject: [PATCH 61/67] Fix terminal mouse coordinate issues Fixes #34184 --- npm-shrinkwrap.json | 2 +- .../parts/terminal/electron-browser/media/terminal.css | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 35551d36615..065aeccefa0 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -574,7 +574,7 @@ "xterm": { "version": "3.0.0", "from": "Tyriar/xterm.js#vscode-release/1.17", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#851dff7253fc9718bb90f691fc79a7c3c542bcfa" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#2d89a723a61c3b2aabb15bd278dc7792466b8ec5" }, "yauzl": { "version": "2.8.0", diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css index 1d9961ab042..e032054ea47 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css @@ -41,7 +41,10 @@ height: 100%; } -.monaco-workbench .panel.integrated-terminal .xterm-viewport, +.monaco-workbench .panel.integrated-terminal .xterm-viewport { + margin-right: -20px; +} + .monaco-workbench .panel.integrated-terminal canvas { /* Align the viewport and canvases to the bottom of the panel */ position: absolute; @@ -71,8 +74,9 @@ } .monaco-workbench .panel.integrated-terminal .xterm { - position: relative; - height: 100%; + position: absolute; + bottom: 0; + left: 0; user-select: none; } From f7962f0682a76516df51de4856f8ccc5d8ad578a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:58:40 +0900 Subject: [PATCH 62/67] Uplevel xterm.js See sourcelair/xterm.js#990 --- npm-shrinkwrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 065aeccefa0..e5f51982d1d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -574,7 +574,7 @@ "xterm": { "version": "3.0.0", "from": "Tyriar/xterm.js#vscode-release/1.17", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#2d89a723a61c3b2aabb15bd278dc7792466b8ec5" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#875b219802d116106d3e05a1731bf895bc95851b" }, "yauzl": { "version": "2.8.0", From d0668fb1671dcc754fc5b1168934a2e64c3e274a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 20 Sep 2017 09:29:14 +0200 Subject: [PATCH 63/67] git: leverage new ViewColumn.Active --- extensions/git/src/commands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 80fad68410e..d49e59d2eeb 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -169,7 +169,7 @@ export class CommandCenter { const opts: TextDocumentShowOptions = { preserveFocus, preview, - viewColumn: window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One + viewColumn: ViewColumn.Active }; const activeTextEditor = window.activeTextEditor; @@ -365,7 +365,7 @@ export class CommandCenter { const opts: TextDocumentShowOptions = { preserveFocus, preview: preview, - viewColumn: activeTextEditor && activeTextEditor.viewColumn || ViewColumn.One + viewColumn: ViewColumn.Active }; if (activeTextEditor && activeTextEditor.document.uri.fsPath === uri.fsPath) { From f56d080258b3c5c32ad0a1e30b21d3fdf78a95c7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 20 Sep 2017 09:43:50 +0200 Subject: [PATCH 64/67] fix npe --- src/vs/workbench/browser/labels.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index ac2e3bc2d34..20399881630 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -210,14 +210,19 @@ export class FileLabel extends ResourceLabel { } public setFile(resource: uri, options?: IFileLabelOptions): void { + const hideLabel = options && options.hideLabel; let name: string; - if (options && options.hideLabel) { - name = void 0; - } else if (options && options.fileKind === FileKind.ROOT_FOLDER) { - const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - name = workspaceFolder.name; - } else { - name = paths.basename(resource.fsPath); + if (!hideLabel) { + if (options && options.fileKind === FileKind.ROOT_FOLDER) { + const workspaceFolder = this.contextService.getWorkspaceFolder(resource); + if (workspaceFolder) { + name = workspaceFolder.name; + } + } + + if (!name) { + name = paths.basename(resource.fsPath); + } } let description: string; From ed20bae44ae9c6aa7084457551211fb8ce19de5b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 20 Sep 2017 09:45:50 +0200 Subject: [PATCH 65/67] introduce IWorkspaceFoldersChangeEvent for onDidChangeWorkspaceFolders (for #31195) --- .../standalone/browser/simpleServices.ts | 6 ++-- src/vs/platform/workspace/common/workspace.ts | 8 ++++- .../configuration/node/configuration.ts | 31 +++++++++++++------ .../workbench/test/workbenchTestServices.ts | 8 ++--- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 3eea14eb51a..23ed80e4d83 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -18,7 +18,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso import { IKeybindingEvent, KeybindingSource, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfirmation, IMessageService } from 'vs/platform/message/common/message'; -import { IWorkspaceContextService, IWorkspace, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace, WorkbenchState, WorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; @@ -525,8 +525,8 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { private readonly _onDidChangeWorkspaceName: Emitter = new Emitter(); public readonly onDidChangeWorkspaceName: Event = this._onDidChangeWorkspaceName.event; - private readonly _onDidChangeWorkspaceFolders: Emitter = new Emitter(); - public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; + private readonly _onDidChangeWorkspaceFolders: Emitter = new Emitter(); + public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; private readonly _onDidChangeWorkbenchState: Emitter = new Emitter(); public readonly onDidChangeWorkbenchState: Event = this._onDidChangeWorkbenchState.event; diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 0e307a99689..e923eb66709 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -21,6 +21,12 @@ export enum WorkbenchState { WORKSPACE } +export interface IWorkspaceFoldersChangeEvent { + added: WorkspaceFolder[]; + removed: WorkspaceFolder[]; + changed: WorkspaceFolder[]; +} + export interface IWorkspaceContextService { _serviceBrand: any; @@ -52,7 +58,7 @@ export interface IWorkspaceContextService { /** * An event which fires on workspace folders change. */ - onDidChangeWorkspaceFolders: Event; + onDidChangeWorkspaceFolders: Event; /** * Returns the folder for the given resource from the workspace. diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index fd6f77db55d..fe168920f34 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -9,7 +9,6 @@ import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; import { StrictResourceMap } from 'vs/base/common/map'; -import { equals } from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; @@ -18,7 +17,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { readFile, stat, writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as extfs from 'vs/base/node/extfs'; -import { IWorkspaceContextService, IWorkspace, Workspace, WorkbenchState, WorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace, Workspace, WorkbenchState, WorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { ConfigWatcher } from 'vs/base/node/config'; @@ -253,8 +252,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat protected readonly _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; - protected readonly _onDidChangeWorkspaceFolders: Emitter = this._register(new Emitter()); - public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; + protected readonly _onDidChangeWorkspaceFolders: Emitter = this._register(new Emitter()); + public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; protected readonly _onDidChangeWorkspaceName: Emitter = this._register(new Emitter()); public readonly onDidChangeWorkspaceName: Event = this._onDidChangeWorkspaceName.event; @@ -438,11 +437,25 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this._onDidChangeWorkspaceName.fire(); } - if (!equals(this.workspace.folders, currentFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath)) { - this._onDidChangeWorkspaceFolders.fire(); + const changes = this.compareFolders(currentFolders, this.workspace.folders); + if (changes.added.length || changes.removed.length || changes.changed.length) { + this._onDidChangeWorkspaceFolders.fire(changes); } } + private compareFolders(currentFolders: WorkspaceFolder[], newFolders: WorkspaceFolder[]): IWorkspaceFoldersChangeEvent { + const result = { added: [], removed: [], changed: [] }; + + result.added = newFolders.filter(newFolder => !currentFolders.some(currentFolder => newFolder.uri.toString() === currentFolder.uri.toString())); + result.removed = currentFolders.filter(currentFolder => !newFolders.some(newFolder => currentFolder.uri.toString() === newFolder.uri.toString())); + + if (result.added.length === 0 && result.removed.length === 0) { + result.changed = currentFolders.filter((currentFolder, index) => newFolders[index].uri.toString() !== currentFolder.uri.toString()); + } + + return result; + } + private initializeConfiguration(trigger: boolean = true): TPromise { this.resetCaches(); return this.updateConfiguration() @@ -487,12 +500,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private onWorkspaceConfigurationChanged(): void { if (this.workspace && this.workspace.configuration) { let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders, URI.file(paths.dirname(this.workspace.configuration.fsPath))); - const foldersChanged = !equals(this.workspace.folders, configuredFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath); - if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes + const changes = this.compareFolders(this.workspace.folders, configuredFolders); + if (changes.added.length || changes.removed.length || changes.changed.length) { // TODO@Sandeep be smarter here about detecting changes this.workspace.folders = configuredFolders; this.onFoldersChanged() .then(configurationChanged => { - this._onDidChangeWorkspaceFolders.fire(); + this._onDidChangeWorkspaceFolders.fire(changes); if (configurationChanged) { this.triggerConfigurationChange(); } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 1502ef8fe6e..7f89866c96d 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -27,7 +27,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorInput, IEditorOptions, Position, Direction, IEditor, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IMessageService, IConfirmation } from 'vs/platform/message/common/message'; -import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, WorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -74,14 +74,14 @@ export class TestContextService implements IWorkspaceContextService { private options: any; private _onDidChangeWorkspaceName: Emitter; - private _onDidChangeWorkspaceFolders: Emitter; + private _onDidChangeWorkspaceFolders: Emitter; private _onDidChangeWorkbenchState: Emitter; constructor(workspace: any = TestWorkspace, options: any = null) { this.workspace = workspace; this.id = generateUuid(); this.options = options || Object.create(null); - this._onDidChangeWorkspaceFolders = new Emitter(); + this._onDidChangeWorkspaceFolders = new Emitter(); this._onDidChangeWorkbenchState = new Emitter(); } @@ -89,7 +89,7 @@ export class TestContextService implements IWorkspaceContextService { return this._onDidChangeWorkspaceName.event; } - public get onDidChangeWorkspaceFolders(): Event { + public get onDidChangeWorkspaceFolders(): Event { return this._onDidChangeWorkspaceFolders.event; } From 54dfbf8c66e457a9ca79e63f9412158a3f7bb895 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 20 Sep 2017 09:54:24 +0200 Subject: [PATCH 66/67] fix #34520 --- src/vs/base/common/htmlContent.ts | 2 +- src/vs/workbench/api/node/extHostTypeConverters.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index ab7ae6065f1..ce013b916cc 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -56,7 +56,7 @@ export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownStri export function isMarkdownString(thing: any): thing is IMarkdownString { if (thing instanceof MarkdownString) { return true; - } else if (typeof thing === 'object') { + } else if (thing && typeof thing === 'object') { return typeof (thing).value === 'string' && (typeof (thing).isTrusted === 'boolean' || (thing).isTrusted === void 0); } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index aa6a0d2b7d5..d8aef198fd8 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -158,7 +158,7 @@ export namespace MarkdownString { } function isCodeblock(thing: any): thing is Codeblock { - return typeof thing === 'object' + return thing && typeof thing === 'object' && typeof (thing).language === 'string' && typeof (thing).value === 'string'; } From 94eaf80a25cd292db2b1230e928b5304eefce0df Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 20 Sep 2017 10:01:56 +0200 Subject: [PATCH 67/67] Use workbench state instead of checking on workspace config --- .../parts/extensions/browser/extensionsActions.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 7640073567b..311b186a859 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -1368,11 +1368,13 @@ export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfi } public run(event: any): TPromise { - const workspace = this.contextService.getWorkspace(); - if (workspace.configuration) { - return this.openWorkspaceConfigurationFile(workspace.configuration); + switch (this.contextService.getWorkbenchState()) { + case WorkbenchState.FOLDER: + return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), this.contextService.getWorkspace().folders[0])); + case WorkbenchState.WORKSPACE: + return this.openWorkspaceConfigurationFile(this.contextService.getWorkspace().configuration); } - return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), workspace.folders[0])); + return TPromise.as(null); } dispose(): void {