diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 66a71634ce3ac..a682ddd9d25f8 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -93,7 +93,7 @@ const BUNDLED_FILE_HEADER = [ ' *--------------------------------------------------------*/' ].join('\n'); -const languages = i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages: []); +const languages = i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); gulp.task('clean-optimized-vscode', util.rimraf('out-vscode')); gulp.task('optimize-vscode', ['clean-optimized-vscode', 'compile-build', 'compile-extensions-build'], common.optimizeTask({ @@ -411,7 +411,7 @@ gulp.task('vscode-translations-push', function () { gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) - ).pipe(i18n.pushXlfFiles(apiHostname, apiName, apiToken)); + ).pipe(i18n.pushXlfFiles(apiHostname, apiName, apiToken)); }); gulp.task('vscode-translations-push-test', function () { @@ -424,7 +424,7 @@ gulp.task('vscode-translations-push-test', function () { gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) - ).pipe(vfs.dest('../vscode-transifex-input')); + ).pipe(vfs.dest('../vscode-transifex-input')); }); gulp.task('vscode-translations-pull', function () { @@ -599,17 +599,3 @@ gulp.task('generate-vscode-configuration', () => { console.error(e.toString()); }); }); - -//#region Built-In Extensions -gulp.task('clean-builtin-extensions', util.rimraf('.build/builtInExtensions')); -gulp.task('download-builtin-extensions', ['clean-builtin-extensions'], function () { - const marketplaceExtensions = es.merge(...builtInExtensions.map(extension => { - return ext.fromMarketplace(extension.name, extension.version) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); - })); - - return marketplaceExtensions - .pipe(util.setExecutableBit(['**/*.sh'])) - .pipe(vfs.dest('.build/builtInExtensions')); -}); -//#endregion diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 0a78f4ccffd55..c744349c7ee74 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -7,26 +7,116 @@ const fs = require('fs'); const path = require('path'); +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); +const es = require('event-stream'); +const rename = require('gulp-rename'); +const vfs = require('vinyl-fs'); +const ext = require('./extensions'); +const util = require('gulp-util'); + const root = path.dirname(path.dirname(__dirname)); +const builtInExtensions = require('../builtInExtensions'); +const controlFilePath = path.join(process.env['HOME'], '.vscode-oss-dev', 'extensions', 'control.json'); + +function getExtensionPath(extension) { + return path.join(root, '.build', 'builtInExtensions', extension.name); +} function isUpToDate(extension) { - const packagePath = path.join(root, '.build', 'builtInExtensions', extension.name, 'package.json'); + const packagePath = path.join(getExtensionPath(extension), 'package.json'); + if (!fs.existsSync(packagePath)) { return false; } + const packageContents = fs.readFileSync(packagePath); + try { const diskVersion = JSON.parse(packageContents).version; return (diskVersion === extension.version); - } catch(err) { + } catch (err) { return false; } } -const builtInExtensions = require('../builtInExtensions'); -builtInExtensions.forEach((extension) => { - if (!isUpToDate(extension)) { - process.exit(1); +function syncMarketplaceExtension(extension) { + if (isUpToDate(extension)) { + util.log(util.colors.blue('[marketplace]'), `${extension.name}@${extension.version}`, util.colors.green('✔︎')); + return es.readArray([]); + } + + rimraf.sync(getExtensionPath(extension)); + + return ext.fromMarketplace(extension.name, extension.version) + .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)) + .pipe(vfs.dest('.build/builtInExtensions')) + .on('end', () => util.log(util.colors.blue('[marketplace]'), extension.name, util.colors.green('✔︎'))); +} + +function syncExtension(extension, controlState) { + switch (controlState) { + case 'disabled': + util.log(util.colors.blue('[disabled]'), util.colors.gray(extension.name)); + rimraf.sync(getExtensionPath(extension)); + return es.readArray([]); + + case 'marketplace': + return syncMarketplaceExtension(extension); + + default: + if (!fs.existsSync(controlState)) { + util.log(util.colors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); + return es.readArray([]); + + } else if (!fs.existsSync(path.join(controlState, 'package.json'))) { + util.log(util.colors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); + return es.readArray([]); + } + + util.log(util.colors.blue('[local]'), `${extension.name}: ${controlState}`, util.colors.green('✔︎')); + return es.readArray([]); } -}); -process.exit(0); +} + +function readControlFile() { + try { + return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + } catch (err) { + return {}; + } +} + +function writeControlFile(control) { + mkdirp.sync(path.dirname(controlFilePath)); + fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); +} + +function main() { + util.log('Syncronizing built-in extensions...'); + util.log('Control file:', controlFilePath); + + const control = readControlFile(); + const streams = []; + + for (const extension of builtInExtensions) { + let controlState = control[extension.name] || 'marketplace'; + control[extension.name] = controlState; + + streams.push(syncExtension(extension, controlState)); + } + + writeControlFile(control); + + es.merge(streams) + .on('error', err => { + console.error(err); + process.exit(1); + }) + .on('end', () => { + util.log(`${streams.length} built-in extensions processed.`); + process.exit(0); + }); +} + +main(); diff --git a/scripts/code.bat b/scripts/code.bat index b23ee223546e8..018577357d70a 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -17,9 +17,8 @@ set CODE=".build\electron\%NAMESHORT%" node build\lib\electron.js if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron -:: Get built-in extensions +:: Sync built-in extensions node build\lib\builtInExtensions.js -if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js download-builtin-extensions :: Build if not exist out node .\node_modules\gulp\bin\gulp.js compile diff --git a/scripts/code.sh b/scripts/code.sh index 7f52ded6fe530..0d9402a6eec73 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -24,8 +24,8 @@ function code() { # Get electron node build/lib/electron.js || ./node_modules/.bin/gulp electron - # Get built-in extensions - node build/lib/builtInExtensions.js || ./node_modules/.bin/gulp download-builtin-extensions + # Sync built-in extensions + node build/lib/builtInExtensions.js # Build test -d out || ./node_modules/.bin/gulp compile diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 05ab5d56cd87d..df2fbfde7d30e 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -17,6 +17,12 @@ export interface IRenderer { disposeTemplate(templateData: TTemplateData): void; } +export interface IListOpenEvent { + elements: T[]; + indexes: number[]; + browserEvent?: UIEvent; +} + export interface IListEvent { elements: T[]; indexes: number[]; diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 2de68902c7cb1..fb3984e469966 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -6,7 +6,7 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; -import { IDelegate, IRenderer, IListEvent } from './list'; +import { IDelegate, IRenderer, IListEvent, IListOpenEvent } from './list'; import { List, IListStyles, IListOptions } from './listWidget'; import { IPagedModel } from 'vs/base/common/paging'; import Event, { mapEvent } from 'vs/base/common/event'; @@ -97,6 +97,10 @@ export class PagedList { return mapEvent(this.list.onFocusChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); } + get onOpen(): Event> { + return mapEvent(this.list.onOpen, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); + } + get onSelectionChange(): Event> { return mapEvent(this.list.onSelectionChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); } @@ -126,8 +130,8 @@ export class PagedList { this.list.scrollTop = scrollTop; } - open(indexes: number[]): void { - this.list.open(indexes); + open(indexes: number[], browserEvent?: UIEvent): void { + this.list.open(indexes, browserEvent); } setFocus(indexes: number[]): void { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d1c7b58200854..804081a26f055 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -15,7 +15,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import Event, { Emitter, EventBufferer, chain, mapEvent, anyEvent } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list'; +import { IDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list'; import { ListView, IListViewOptions } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -261,6 +261,7 @@ function isInputElement(e: HTMLElement): boolean { class KeyboardController implements IDisposable { private disposables: IDisposable[]; + private openController: IOpenController; constructor( private list: List, @@ -270,6 +271,8 @@ class KeyboardController implements IDisposable { const multipleSelectionSupport = !(options.multipleSelectionSupport === false); this.disposables = []; + this.openController = options.openController || DefaultOpenController; + const onKeyDown = chain(domEvent(view.domNode, 'keydown')) .filter(e => !isInputElement(e.target as HTMLElement)) .map(e => new StandardKeyboardEvent(e)); @@ -290,7 +293,10 @@ class KeyboardController implements IDisposable { e.preventDefault(); e.stopPropagation(); this.list.setSelection(this.list.getFocus()); - this.list.open(this.list.getFocus()); + + if (this.openController.shouldOpen(e.browserEvent)) { + this.list.open(this.list.getFocus(), e.browserEvent); + } } private onUpArrow(e: StandardKeyboardEvent): void { @@ -357,10 +363,15 @@ const DefaultMultipleSelectionContoller = { isSelectionRangeChangeEvent }; +const DefaultOpenController = { + shouldOpen: (event: UIEvent) => true +}; + class MouseController implements IDisposable { private multipleSelectionSupport: boolean; - private multipleSelectionController: IMultipleSelectionController | undefined; + private multipleSelectionController: IMultipleSelectionController; + private openController: IOpenController; private didJustPressContextMenuKey: boolean = false; private disposables: IDisposable[] = []; @@ -406,6 +417,8 @@ class MouseController implements IDisposable { this.multipleSelectionController = options.multipleSelectionController || DefaultMultipleSelectionContoller; } + this.openController = options.openController || DefaultOpenController; + view.onMouseDown(this.onMouseDown, this, this.disposables); view.onMouseClick(this.onPointer, this, this.disposables); view.onMouseDblClick(this.onDoubleClick, this, this.disposables); @@ -458,7 +471,10 @@ class MouseController implements IDisposable { if (this.options.selectOnMouseDown) { this.list.setSelection([focus]); - this.list.open([focus]); + + if (this.openController.shouldOpen(e.browserEvent)) { + this.list.open([focus], e.browserEvent); + } } } @@ -470,7 +486,10 @@ class MouseController implements IDisposable { if (!this.options.selectOnMouseDown) { const focus = this.list.getFocus(); this.list.setSelection(focus); - this.list.open(focus); + + if (this.openController.shouldOpen(e.browserEvent)) { + this.list.open(focus, e.browserEvent); + } } } @@ -523,6 +542,10 @@ export interface IMultipleSelectionController { isSelectionRangeChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean; } +export interface IOpenController { + shouldOpen(event: UIEvent): boolean; +} + export interface IListOptions extends IListViewOptions, IListStyles { identityProvider?: IIdentityProvider; ariaLabel?: string; @@ -533,6 +556,7 @@ export interface IListOptions extends IListViewOptions, IListStyles { verticalScrollMode?: ScrollbarVisibility; multipleSelectionSupport?: boolean; multipleSelectionController?: IMultipleSelectionController; + openController?: IOpenController; } export interface IListStyles { @@ -708,9 +732,9 @@ export class List implements ISpliceable, IDisposable { readonly onContextMenu: Event> = Event.None; - private _onOpen = new Emitter(); - @memoize get onOpen(): Event> { - return mapEvent(this._onOpen.event, indexes => this.toListEvent({ indexes })); + private _onOpen = new Emitter>(); + @memoize get onOpen(): Event> { + return this._onOpen.event; } private _onPin = new Emitter(); @@ -973,8 +997,8 @@ export class List implements ISpliceable, IDisposable { return this.view.domNode; } - open(indexes: number[]): void { - this._onOpen.fire(indexes); + open(indexes: number[], browserEvent?: UIEvent): void { + this._onOpen.fire({ indexes, elements: indexes.map(i => this.view.element(i)), browserEvent }); } pin(indexes: number[]): void { diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index 05e19027d4f21..845ec66aa4cdf 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -38,8 +38,14 @@ export enum ClickBehavior { ON_MOUSE_UP } +export enum OpenMode { + SINGLE_CLICK, + DOUBLE_CLICK +} + export interface IControllerOptions { clickBehavior?: ClickBehavior; + openMode?: OpenMode; keyboardSupport?: boolean; } @@ -82,7 +88,7 @@ export class DefaultController implements _.IController { private options: IControllerOptions; - constructor(options: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: true }) { + constructor(options: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: true, openMode: OpenMode.SINGLE_CLICK }) { this.options = options; this.downKeyBindingDispatcher = new KeybindingDispatcher(); @@ -153,6 +159,7 @@ export class DefaultController implements _.IController { protected onLeftClick(tree: _.ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean { const payload = { origin: origin, originalEvent: eventish }; + const isDoubleClick = (origin === 'mouse' && (eventish).detail === 2); if (tree.getInput() === element) { tree.clearFocus(payload); @@ -168,16 +175,26 @@ export class DefaultController implements _.IController { tree.setSelection([element], payload); tree.setFocus(element, payload); - if (tree.isExpanded(element)) { - tree.collapse(element).done(null, errors.onUnexpectedError); - } else { - tree.expand(element).done(null, errors.onUnexpectedError); + if (this.openOnSingleClick || isDoubleClick) { + if (tree.isExpanded(element)) { + tree.collapse(element).done(null, errors.onUnexpectedError); + } else { + tree.expand(element).done(null, errors.onUnexpectedError); + } } } return true; } + protected setOpenMode(openMode: OpenMode) { + this.options.openMode = openMode; + } + + protected get openOnSingleClick(): boolean { + return this.options.openMode === OpenMode.SINGLE_CLICK; + } + public onContextMenu(tree: _.ITree, element: any, event: _.ContextMenuEvent): boolean { if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') { return false; // allow context menu on input fields diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 136e54c22a846..e5fff92bdaef4 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -23,7 +23,6 @@ import { GestureEvent } from 'vs/base/browser/touch'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { FileLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import * as tree from 'vs/base/parts/tree/browser/tree'; -import { DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Range, IRange } from 'vs/editor/common/core/range'; @@ -41,7 +40,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import URI from 'vs/base/common/uri'; import { TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Location } from 'vs/editor/common/modes'; @@ -218,7 +217,7 @@ class DataSource implements tree.IDataSource { } } -class Controller extends DefaultController { +class Controller extends WorkbenchTreeController { private _onDidFocus = new Emitter(); readonly onDidFocus: Event = this._onDidFocus.event; @@ -243,19 +242,22 @@ class Controller extends DefaultController { } public onMouseDown(tree: tree.ITree, element: any, event: IMouseEvent): boolean { + var isDoubleClick = event.detail === 2; if (event.leftButton) { if (element instanceof FileReferences) { - event.preventDefault(); - event.stopPropagation(); - return this._expandCollapse(tree, element); + if (this.openOnSingleClick || isDoubleClick) { + event.preventDefault(); + event.stopPropagation(); + return this._expandCollapse(tree, element); + } } var result = super.onClick(tree, element, event); if (event.ctrlKey || event.metaKey || event.altKey) { this._onDidOpenToSide.fire(element); - } else if (event.detail === 2) { + } else if (isDoubleClick) { this._onDidSelect.fire(element); - } else { + } else if (this.openOnSingleClick) { this._onDidFocus.fire(element); } return result; @@ -631,7 +633,9 @@ export class ReferenceWidget extends PeekViewWidget { // tree container.div({ 'class': 'ref-tree inline' }, (div: Builder) => { - const controller = new Controller(); + var controller = this._instantiationService.createInstance(Controller, {}); + this._callOnDispose.push(controller); + var config = { dataSource: this._instantiationService.createInstance(DataSource), renderer: this._instantiationService.createInstance(Renderer), @@ -641,8 +645,7 @@ export class ReferenceWidget extends PeekViewWidget { var options: tree.ITreeOptions = { twistiePixels: 20, - ariaLabel: nls.localize('treeAriaLabel', "References"), - keyboardSupport: false + ariaLabel: nls.localize('treeAriaLabel', "References") }; this._tree = this._instantiationService.createInstance(WorkbenchTree, div.getHTMLElement(), config, options); diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 30a06931cd72f..218e5ec9fd554 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -444,8 +444,7 @@ export class SnippetSession { return false; } - let ranges: Range[] = []; - let placeholderIndex: number = -1; + let allPossibleSelections: Map; for (const snippet of this._snippets) { const possibleSelections = snippet.computePossibleSelections(); @@ -453,49 +452,58 @@ export class SnippetSession { // for the first snippet find the placeholder (and its ranges) // that contain at least one selection. for all remaining snippets // the same placeholder (and their ranges) must be used. - if (placeholderIndex < 0) { + if (!allPossibleSelections) { + allPossibleSelections = new Map(); possibleSelections.forEach((ranges, index) => { - if (placeholderIndex >= 0) { - return; - } + ranges.sort(Range.compareRangesUsingStarts); for (const selection of selections) { if (ranges[0].containsRange(selection)) { - placeholderIndex = index; + allPossibleSelections.set(index, []); break; } } }); } - if (placeholderIndex < 0) { + if (allPossibleSelections.size === 0) { // return false if we couldn't associate a selection to // this (the first) snippet return false; } - ranges.push(...possibleSelections.get(placeholderIndex)); - } - - if (selections.length !== ranges.length) { - // this means we started at a placeholder with N - // ranges and new have M (N > M) selections. - // So (at least) one placeholder is without selection -> cancel - return false; + // add selections from 'this' snippet so that we know all + // selections for this placeholder + allPossibleSelections.forEach((array, index) => { + array.push(...possibleSelections.get(index)); + }); } - // also sort (placeholder)-ranges. then walk both arrays and - // make sure the placeholder-ranges contain the corresponding + // sort selections (and later placeholder-ranges). then walk both + // arrays and make sure the placeholder-ranges contain the corresponding // selection selections.sort(Range.compareRangesUsingStarts); - ranges.sort(Range.compareRangesUsingStarts); - for (let i = 0; i < ranges.length; i++) { - if (!ranges[i].containsRange(selections[i])) { - return false; + allPossibleSelections.forEach((ranges, index) => { + + if (ranges.length !== selections.length) { + allPossibleSelections.delete(index); + return; } - } - return true; + ranges.sort(Range.compareRangesUsingStarts); + + for (let i = 0; i < ranges.length; i++) { + if (!ranges[i].containsRange(selections[i])) { + allPossibleSelections.delete(index); + return; + } + } + }); + + // from all possible selections we have deleted those + // that don't match with the current selection. if we don't + // have any left, we don't have a selection anymore + return allPossibleSelections.size > 0; } } diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts index 0bcbb92e57983..526dcb3dc1a18 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts @@ -303,4 +303,18 @@ suite('SnippetController2', function () { assertSelections(editor, new Selection(2, 9, 2, 9), new Selection(1, 7, 1, 7)); assertContextKeys(contextKeys, true, false, true); }); + + test('“Nested” snippets terminating abruptly in VSCode 1.19.2. #42012', function () { + + const ctrl = new SnippetController2(editor, logService, contextKeys); + model.setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + ctrl.insert('var ${2:${1:name}} = ${1:name} + 1;${0}'); + + assertSelections(editor, new Selection(1, 5, 1, 9), new Selection(1, 12, 1, 16)); + assertContextKeys(contextKeys, true, false, true); + + ctrl.next(); + assertContextKeys(contextKeys, true, true, true); + }); }); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4688278bde0ea..33fe4d5af2c2d 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -273,7 +273,7 @@ export class ExtensionManagementService implements IExtensionManagementService { installingExtension = this.getExtensionsReport() .then(report => { if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { - throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be malicious.")); + throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); } else { return extension; } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index d283b200549ef..403261b068753 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -5,9 +5,9 @@ 'use strict'; import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; -import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; +import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController, IOpenController } from 'vs/base/browser/ui/list/listWidget'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; @@ -20,7 +20,10 @@ import { mixin } from 'vs/base/common/objects'; import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; - +import { DefaultController, IControllerOptions, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import Event, { Emitter } from 'vs/base/common/event'; export type ListWidget = List | PagedList | ITree; export const IListService = createDecorator('listService'); @@ -94,12 +97,17 @@ function createScopedContextKeyService(contextKeyService: IContextKeyService, wi return result; } -export const multiSelectModifierSettingKey = 'workbench.multiSelectModifier'; +export const multiSelectModifierSettingKey = 'workbench.list.multiSelectModifier'; +export const openModeSettingKey = 'workbench.list.openMode'; -export function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { +function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; } +function useSingleClickToOpen(configurationService: IConfigurationService): boolean { + return configurationService.getValue(openModeSettingKey) !== 'doubleClick'; +} + class MultipleSelectionController implements IMultipleSelectionController { constructor(private configurationService: IConfigurationService) { } @@ -117,11 +125,39 @@ class MultipleSelectionController implements IMultipleSelectionController } } +class OpenController implements IOpenController { + + constructor(private configurationService: IConfigurationService) { } + + shouldOpen(event: UIEvent): boolean { + if (event instanceof MouseEvent) { + const isDoubleClick = event.detail === 2; + + return useSingleClickToOpen(this.configurationService) || isDoubleClick; + } + + return true; + } +} + +function handleListControllers(options: IListOptions, configurationService: IConfigurationService): IListOptions { + if (options.multipleSelectionSupport === true && !options.multipleSelectionController) { + options.multipleSelectionController = new MultipleSelectionController(configurationService); + } + + options.openController = new OpenController(configurationService); + + return options; +} + export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; + private listDoubleSelection: IContextKey; + private _useAltAsMultipleSelectionModifier: boolean; + constructor( container: HTMLElement, delegate: IDelegate, @@ -130,31 +166,45 @@ export class WorkbenchList extends List { @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService ) { - const multipleSelectionSupport = !(options.multipleSelectionSupport === false); + super(container, delegate, renderers, mixin(handleListControllers(options, configurationService), { keyboardSupport: false } as IListOptions, false)); - if (multipleSelectionSupport && !options.multipleSelectionController) { - options.multipleSelectionController = new MultipleSelectionController(configurationService); - } - - super(container, delegate, renderers, mixin(options, useAltAsMultipleSelectionModifier(configurationService))); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + this.disposables.push(combinedDisposable([ this.contextKeyService, (listService as ListService).register(this), attachListStyler(this, themeService), this.onSelectionChange(() => this.listDoubleSelection.set(this.getSelection().length === 2)) ])); + + this.registerListeners(); + } + + public get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } + + private registerListeners(): void { + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + } + })); } } export class WorkbenchPagedList extends PagedList { readonly contextKeyService: IContextKeyService; - private disposable: IDisposable; + + private disposables: IDisposable[] = []; + + private _useAltAsMultipleSelectionModifier: boolean; constructor( container: HTMLElement, @@ -164,57 +214,134 @@ export class WorkbenchPagedList extends PagedList { @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService ) { - const multipleSelectionSupport = !(options.multipleSelectionSupport === false); + super(container, delegate, renderers, mixin(handleListControllers(options, configurationService), { keyboardSupport: false } as IListOptions, false)); - if (multipleSelectionSupport && !options.multipleSelectionController) { - options.multipleSelectionController = new MultipleSelectionController(configurationService); - } - - super(container, delegate, renderers, mixin(options, useAltAsMultipleSelectionModifier(configurationService))); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); - this.disposable = combinedDisposable([ + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + + this.disposables.push(combinedDisposable([ this.contextKeyService, (listService as ListService).register(this), attachListStyler(this, themeService) - ]); + ])); + + this.registerListeners(); + } + + public get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } + + private registerListeners(): void { + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + } + })); } dispose(): void { - this.disposable.dispose(); + this.disposables = dispose(this.disposables); } } export class WorkbenchTree extends Tree { readonly contextKeyService: IContextKeyService; + protected disposables: IDisposable[] = []; + private listDoubleSelection: IContextKey; + private _openOnSingleClick: boolean; + private _useAltAsMultipleSelectionModifier: boolean; + constructor( container: HTMLElement, configuration: ITreeConfiguration, options: ITreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @IConfigurationService private configurationService: IConfigurationService ) { - super(container, configuration, options); + super(container, configuration, mixin(options, { keyboardSupport: false } as ITreeOptions, false)); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); + this._openOnSingleClick = useSingleClickToOpen(configurationService); + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + this.disposables.push( this.contextKeyService, (listService as ListService).register(this), attachListStyler(this, themeService) ); + + this.registerListeners(); + } + + public get openOnSingleClick(): boolean { + return this._openOnSingleClick; + } + + public get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } + + private registerListeners(): void { this.disposables.push(this.onDidChangeSelection(() => { const selection = this.getSelection(); this.listDoubleSelection.set(selection && selection.length === 2); })); + + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(openModeSettingKey)) { + this._openOnSingleClick = useSingleClickToOpen(this.configurationService); + } + + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + } + })); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export class WorkbenchTreeController extends DefaultController { + + protected disposables: IDisposable[] = []; + + constructor( + options: IControllerOptions, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(options); + + // if the open mode is not set, we configure it based on settings + if (isUndefinedOrNull(options.openMode)) { + this.setOpenMode(this.getOpenModeSetting()); + this.registerListeners(); + } + } + + private registerListeners(): void { + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(openModeSettingKey)) { + this.setOpenMode(this.getOpenModeSetting()); + } + })); + } + + private getOpenModeSetting(): OpenMode { + return useSingleClickToOpen(this.configurationService) ? OpenMode.SINGLE_CLICK : OpenMode.DOUBLE_CLICK; } dispose(): void { @@ -222,6 +349,89 @@ export class WorkbenchTree extends Tree { } } +export interface IOpenResourceOptions { + editorOptions: IEditorOptions; + sideBySide: boolean; + element: any; + payload: any; +} + +export interface IResourceResultsNavigationOptions { + openOnFocus: boolean; +} + +export default class ResourceResultsNavigation extends Disposable { + + private _openResource: Emitter = new Emitter(); + public readonly openResource: Event = this._openResource.event; + + constructor(private tree: WorkbenchTree, private options?: IResourceResultsNavigationOptions) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + if (this.options && this.options.openOnFocus) { + this._register(this.tree.onDidChangeFocus(e => this.onFocus(e))); + } + + this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); + } + + private onFocus({ payload }: any): void { + const element = this.tree.getFocus(); + this.tree.setSelection([element], { fromFocus: true }); + + const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; + const isMouseEvent = payload && payload.origin === 'mouse'; + const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; + + if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + this._openResource.fire({ + editorOptions: { + preserveFocus: true, + pinned: false, + revealIfVisible: true + }, + sideBySide: false, + element, + payload + }); + } + } + + private onSelection({ payload }: any): void { + if (payload && payload.fromFocus) { + return; + } + + const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; + const isMouseEvent = payload && payload.origin === 'mouse'; + const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; + + if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + if (isDoubleClick && originalEvent) { + originalEvent.preventDefault(); // focus moves to editor, we need to prevent default + } + + const isFromKeyboard = payload && payload.origin === 'keyboard'; + const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey || originalEvent.altKey)); + const preserveFocus = !((isFromKeyboard && (!payload || !payload.preserveFocus)) || isDoubleClick || (payload && payload.focusEditor)); + this._openResource.fire({ + editorOptions: { + preserveFocus, + pinned: isDoubleClick, + revealIfVisible: true + }, + sideBySide, + element: this.tree.getSelection()[0], + payload + }); + } + } +} + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ @@ -230,7 +440,7 @@ configurationRegistry.registerConfiguration({ 'title': localize('workbenchConfigurationTitle', "Workbench"), 'type': 'object', 'properties': { - 'workbench.multiSelectModifier': { + 'workbench.list.multiSelectModifier': { 'type': 'string', 'enum': ['ctrlCmd', 'alt'], 'enumDescriptions': [ @@ -244,7 +454,20 @@ configurationRegistry.registerConfiguration({ '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add an item to a multi-selection with the mouse (for example in trees and lists, if supported). `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") + }, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (if supported). `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") + }, + 'workbench.list.openMode': { + 'type': 'string', + 'enum': ['singleClick', 'doubleClick'], + 'enumDescriptions': [ + localize('openMode.singleClick', "Opens items on mouse single click."), + localize('openMode.doubleClick', "Open items on mouse double click.") + ], + 'default': 'singleClick', + 'description': localize({ + key: 'openModeModifier', + comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] + }, "Controls how to open items in trees and lists using the mouse (if supported). Set to `singleClick` to open items with a single mouse click and `doubleClick` to only open via mouse double click. For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might chose to ignore this setting if it is not applicable. ") } } }); \ No newline at end of file diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index c488cd29b5763..8c6a5442b8ea5 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5495,13 +5495,14 @@ declare module 'vscode' { * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) * to restrict the search results to a [workspace folder](#WorkspaceFolder). * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no * [workspace folders](#workspace.workspaceFolders) are opened. */ - export function findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable; + export function findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Thenable; /** * Save all dirty files. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 802c06809ffb3..92abf31ae24fe 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -159,7 +159,7 @@ declare module 'vscode' { export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider): Disposable; /** - * Updates the workspace folders of the currently opened workspace. This method allows to add, remove + * Updates the [workspace folders](#workspace.workspaceFolders) of the currently opened workspace. This method allows to add, remove * and change workspace folders a the same time. Use the [onDidChangeWorkspaceFolders()](#onDidChangeWorkspaceFolders) * event to get notified when the workspace folders have been updated. * @@ -181,11 +181,11 @@ declare module 'vscode' { * It is valid to remove an existing workspace folder and add it again with a different name * to rename that folder. * - * Note: if the first workspace folder is added, removed or changed, all extensions will be restarted + * **Note:** if the first workspace folder is added, removed or changed, all extensions will be restarted * so that the (deprecated) `rootPath` property is updated to point to the first workspace * folder. * - * Note: it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times + * **Note:** it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times * without waiting for the [onDidChangeWorkspaceFolders()](#onDidChangeWorkspaceFolders) to fire. * * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index b1cd93ae038a8..721a2df3e409a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -97,7 +97,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- search --- - $startSearch(includePattern: string, includeFolder: string, excludePattern: string, maxResults: number, requestId: number): Thenable { + $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable { const workspace = this._contextService.getWorkspace(); if (!workspace.folders.length) { return undefined; @@ -129,7 +129,8 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { type: QueryType.File, maxResults, includePattern: { [typeof includePattern === 'string' ? includePattern : undefined]: true }, - excludePattern: { [typeof excludePattern === 'string' ? excludePattern : undefined]: true }, + excludePattern: { [typeof excludePatternOrDisregardExcludes === 'string' ? excludePatternOrDisregardExcludes : undefined]: true }, + disregardExcludeSettings: excludePatternOrDisregardExcludes === false, useRipgrep, ignoreSymlinks }; @@ -168,4 +169,4 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return result.results.every(each => each.success === true); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index e289cf18c822f..cd5e404db1465 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -346,7 +346,7 @@ export interface MainThreadTelemetryShape extends IDisposable { } export interface MainThreadWorkspaceShape extends IDisposable { - $startSearch(includePattern: string, includeFolder: string, excludePattern: string, maxResults: number, requestId: number): Thenable; + $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 38ce5b9188971..366a64d45f480 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -603,11 +603,11 @@ export function toGlobPattern(pattern: vscode.GlobPattern): string | IRelativePa return pattern; } - if (!isRelativePattern(pattern)) { - return undefined; + if (isRelativePattern(pattern)) { + return new types.RelativePattern(pattern.base, pattern.pattern); } - return new types.RelativePattern(pattern.base, pattern.pattern); + return pattern; // preserve `undefined` and `null` } function isRelativePattern(obj: any): obj is vscode.RelativePattern { diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 5ead32afa25fa..fabbcd0ab667d 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -222,7 +222,9 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } // Try to accept directly - return this.trySetWorkspaceFolders(newWorkspaceFolders); + this.trySetWorkspaceFolders(newWorkspaceFolders); + + return true; } getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder { @@ -281,7 +283,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { return normalize(result, true); } - private trySetWorkspaceFolders(folders: vscode.WorkspaceFolder[]): boolean { + private trySetWorkspaceFolders(folders: vscode.WorkspaceFolder[]): void { // Update directly here. The workspace is unconfirmed as long as we did not get an // acknowledgement from the main side (via $acceptWorkspaceData) @@ -292,11 +294,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { configuration: this._actualWorkspace.configuration, folders } as IWorkspaceData, this._actualWorkspace).workspace; - - return true; } - - return false; } $acceptWorkspaceData(data: IWorkspaceData): void { @@ -331,16 +329,18 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } } - let excludePattern: string; - if (exclude) { + let excludePatternOrDisregardExcludes: string | false; + if (exclude === null) { + excludePatternOrDisregardExcludes = false; + } else if (exclude) { if (typeof exclude === 'string') { - excludePattern = exclude; + excludePatternOrDisregardExcludes = exclude; } else { - excludePattern = exclude.pattern; + excludePatternOrDisregardExcludes = exclude.pattern; } } - const result = this._proxy.$startSearch(includePattern, includeFolder, excludePattern, maxResults, requestId); + const result = this._proxy.$startSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, requestId); if (token) { token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId)); } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index f31156a4099a6..87d86c88beea4 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -270,17 +270,17 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_U), - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const model = editorGroupService.getStacksModel(); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); + const contexts = getMultiSelectedEditorContexts(context, accessor.get(IListService)); let positionOne: { unmodifiedOnly: boolean } = void 0; let positionTwo: { unmodifiedOnly: boolean } = void 0; let positionThree: { unmodifiedOnly: boolean } = void 0; contexts.forEach(c => { - switch (model.positionOfGroup(c.group)) { + switch (model.positionOfGroup(model.getGroup(c.groupId))) { case Position.ONE: positionOne = { unmodifiedOnly: true }; break; case Position.TWO: positionTwo = { unmodifiedOnly: true }; break; case Position.THREE: positionThree = { unmodifiedOnly: true }; break; @@ -296,14 +296,15 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_W), - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); - const distinctGroups = distinct(contexts.map(c => c.group)); + const contexts = getMultiSelectedEditorContexts(context, accessor.get(IListService)); + const distinctGroupIds = distinct(contexts.map(c => c.groupId)); + const model = editorGroupService.getStacksModel(); - if (distinctGroups.length) { - return editorService.closeEditors(distinctGroups.map(g => editorGroupService.getStacksModel().positionOfGroup(g))); + if (distinctGroupIds.length) { + return editorService.closeEditors(distinctGroupIds.map(gid => model.positionOfGroup(model.getGroup(gid)))); } const activeEditor = editorService.getActiveEditor(); if (activeEditor) { @@ -320,21 +321,23 @@ function registerEditorCommands() { when: void 0, primary: KeyMod.CtrlCmd | KeyCode.KEY_W, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KEY_W] }, - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); - const groups = distinct(contexts.map(context => context.group)); + const contexts = getMultiSelectedEditorContexts(context, accessor.get(IListService)); + const groupIds = distinct(contexts.map(context => context.groupId)); + const model = editorGroupService.getStacksModel(); const editorsToClose = new Map(); - groups.forEach(group => { - const position = editorGroupService.getStacksModel().positionOfGroup(group); + groupIds.forEach(groupId => { + const group = model.getGroup(groupId); + const position = model.positionOfGroup(group); if (position >= 0) { editorsToClose.set(position, contexts.map(c => { - if (group === c.group) { - let input = c ? c.editor : void 0; + if (c && groupId === c.groupId) { + let input = group.getEditor(c.editorIndex); if (!input) { // Get Top Editor at Position @@ -373,24 +376,26 @@ function registerEditorCommands() { when: void 0, primary: void 0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_T }, - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); - const groups = distinct(contexts.map(context => context.group)); + const contexts = getMultiSelectedEditorContexts(context, accessor.get(IListService)); + const groupIds = distinct(contexts.map(context => context.groupId)); const editorsToClose = new Map(); + const model = editorGroupService.getStacksModel(); - groups.forEach(group => { + groupIds.forEach(groupId => { + const group = model.getGroup(groupId); const inputsToSkip = contexts.map(c => { - if (!!c.editor && c.group === group) { - return c.editor; + if (c.groupId === groupId) { + return group.getEditor(c.editorIndex); } return void 0; }).filter(input => !!input); const toClose = group.getEditors().filter(input => inputsToSkip.indexOf(input) === -1); - editorsToClose.set(editorGroupService.getStacksModel().positionOfGroup(group), toClose); + editorsToClose.set(model.positionOfGroup(group), toClose); }); return editorService.closeEditors({ @@ -406,7 +411,7 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: void 0, - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); @@ -425,7 +430,7 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter), - handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); @@ -489,12 +494,13 @@ function registerEditorCommands() { }); } -function positionAndInput(editorGroupService: IEditorGroupService, editorService: IWorkbenchEditorService, context?: IEditorIdentifier | IEditorCommandsContext): { position: Position, input: IEditorInput } { +function positionAndInput(editorGroupService: IEditorGroupService, editorService: IWorkbenchEditorService, context?: IEditorCommandsContext): { position: Position, input: IEditorInput } { // Resolve from context - const editorContext = toEditorIdentifier(context, editorGroupService); - let position = editorContext ? editorGroupService.getStacksModel().positionOfGroup(editorContext.group) : null; - let input = editorContext ? editorContext.editor : null; + const model = editorGroupService.getStacksModel(); + const group = context ? model.getGroup(context.groupId) : undefined; + let position = group ? model.positionOfGroup(group) : undefined; + let input = group ? group.getEditor(context.editorIndex) : undefined; // If position or input are not passed in take the position and input of the active editor. const active = editorService.getActiveEditor(); @@ -506,54 +512,28 @@ function positionAndInput(editorGroupService: IEditorGroupService, editorService return { position, input }; } -export function getMultiSelectedEditorContexts(editorContext: IEditorIdentifier, listService: IListService): IEditorIdentifier[] { - const elementToContext = (element: IEditorIdentifier | EditorGroup) => element instanceof EditorGroup ? { group: element, editor: void 0 } : element; +export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsContext, listService: IListService): IEditorCommandsContext[] { + const elementToContext = (element: IEditorIdentifier | EditorGroup) => + element instanceof EditorGroup ? { groupId: element.id, editorIndex: undefined } : { groupId: element.group.id, editorIndex: element.group.indexOf(element.editor) }; // First check for a focused list to return the selected items from const list = listService.lastFocusedList; if (list instanceof List && list.isDOMFocused()) { - const selection = list.getSelectedElements(); - const focus = list.getFocusedElements(); - - // Only respect selection if it contains focused element - if (focus.length && selection && selection.indexOf(focus[0]) >= 0) { - return list.getSelectedElements().filter(e => e instanceof EditorGroup || isEditorIdentifier(e)).map(elementToContext); - } + const focusedElements: (IEditorIdentifier | EditorGroup)[] = list.getFocusedElements(); + // need to take into account when editor context is { group: group } + const focus = editorContext ? editorContext : focusedElements.length ? focusedElements.map(elementToContext)[0] : undefined; + + if (focus) { + const selection: (IEditorIdentifier | EditorGroup)[] = list.getSelectedElements(); + // Only respect selection if it contains focused element + if (selection && selection.some(s => s instanceof EditorGroup ? s.id === focus.groupId : s.group.id === focus.groupId && s.group.indexOf(s.editor) === focus.editorIndex)) { + return selection.map(elementToContext); + } - if (focus.length) { - return focus.filter(e => e instanceof EditorGroup || isEditorIdentifier(e)).map(elementToContext); + return [focus]; } } // Otherwise go with passed in context return !!editorContext ? [editorContext] : []; } - -function isEditorIdentifier(object: any): object is IEditorIdentifier { - const identifier = object as IEditorIdentifier; - - return identifier && !!identifier.group && !!identifier.editor; -} - -function isEditorGroupContext(object: any): object is IEditorCommandsContext { - const context = object as IEditorCommandsContext; - - return context && typeof context.groupId === 'number'; -} - -function toEditorIdentifier(object: IEditorIdentifier | IEditorCommandsContext, editorGroupService: IEditorGroupService): IEditorIdentifier { - if (isEditorIdentifier(object)) { - return object as IEditorIdentifier; - } - - if (isEditorGroupContext(object)) { - const stacks = editorGroupService.getStacksModel(); - const group = stacks.getGroup(object.groupId); - return { - group, - editor: typeof object.editorIndex === 'number' ? group.getEditor(object.editorIndex) : void 0 - }; - } - - return void 0; -} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 892dc38d274eb..721af30315571 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -635,7 +635,7 @@ export class TabsTitleControl extends TitleControl { return void 0; // only for left mouse click } - const { editor, position } = this.toTabContext(index); + const { editor, position } = this.getGroupPositionAndEditor(index); if (!this.isTabActionBar(((e as GestureEvent).initialTarget || e.target || e.srcElement) as HTMLElement)) { setTimeout(() => this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError)); // timeout to keep focus in editor after mouse up } @@ -646,7 +646,7 @@ export class TabsTitleControl extends TitleControl { const showContextMenu = (e: Event) => { DOM.EventHelper.stop(e); - const { group, editor } = this.toTabContext(index); + const { group, editor } = this.getGroupPositionAndEditor(index); this.onContextMenu({ group, editor }, e, tab); }; @@ -668,7 +668,7 @@ export class TabsTitleControl extends TitleControl { tab.blur(); if (e.button === 1 /* Middle Button*/ && !this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) { - this.closeEditorAction.run(this.toTabContext(index)).done(null, errors.onUnexpectedError); + this.closeEditorAction.run(this.getGroupPositionAndEditor(index)).done(null, errors.onUnexpectedError); } })); @@ -690,7 +690,7 @@ export class TabsTitleControl extends TitleControl { const event = new StandardKeyboardEvent(e); let handled = false; - const { group, position, editor } = this.toTabContext(index); + const { group, position, editor } = this.getGroupPositionAndEditor(index); // Run action on Enter/Space if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { @@ -733,7 +733,7 @@ export class TabsTitleControl extends TitleControl { disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => { DOM.EventHelper.stop(e); - const { group, editor } = this.toTabContext(index); + const { group, editor } = this.getGroupPositionAndEditor(index); this.editorGroupService.pinEditor(group, editor); })); @@ -741,14 +741,14 @@ export class TabsTitleControl extends TitleControl { // Context menu disposables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => { DOM.EventHelper.stop(e, true); - const { group, editor } = this.toTabContext(index); + const { group, editor } = this.getGroupPositionAndEditor(index); this.onContextMenu({ group, editor }, e, tab); }, true /* use capture to fix https://github.com/Microsoft/vscode/issues/19145 */)); // Drag start disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => { - const { group, editor } = this.toTabContext(index); + const { group, editor } = this.getGroupPositionAndEditor(index); this.onEditorDragStart({ editor, group }); e.dataTransfer.effectAllowed = 'copyMove'; @@ -795,7 +795,7 @@ export class TabsTitleControl extends TitleControl { let draggedEditorIsTab = false; const draggedEditor = TabsTitleControl.getDraggedEditor(); if (draggedEditor) { - const { group, editor } = this.toTabContext(index); + const { group, editor } = this.getGroupPositionAndEditor(index); if (draggedEditor.editor === editor && draggedEditor.group === group) { draggedEditorIsTab = true; } @@ -832,7 +832,7 @@ export class TabsTitleControl extends TitleControl { DOM.removeClass(tab, 'dragged-over'); this.updateDropFeedback(tab, false, index); - const { group, position } = this.toTabContext(index); + const { group, position } = this.getGroupPositionAndEditor(index); this.onDrop(e, group, position, index); })); @@ -844,7 +844,7 @@ export class TabsTitleControl extends TitleControl { return !!DOM.findParentWithClass(element, 'monaco-action-bar', 'tab'); } - private toTabContext(index: number): { group: IEditorGroup, position: Position, editor: IEditorInput } { + private getGroupPositionAndEditor(index: number): { group: IEditorGroup, position: Position, editor: IEditorInput } { const group = this.context; const position = this.stacks.positionOfGroup(group); const editor = group.getEditor(index); @@ -907,7 +907,7 @@ class TabActionRunner extends ActionRunner { return TPromise.as(void 0); } - return super.run(action, { group, editor: group.getEditor(this.index) }); + return super.run(action, { groupId: group.id, editorIndex: this.index }); } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 98235ba28b66d..9a2137363ef47 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -14,7 +14,7 @@ import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions'; import { IMessageService } from 'vs/platform/message/common/message'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; +import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { createActionItem, fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; @@ -26,12 +26,13 @@ import { ViewsRegistry, TreeItemCollapsibleState, ITreeItem, ITreeViewDataProvid import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IViewletViewOptions, IViewOptions, TreeViewsViewletPanel, FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import URI from 'vs/base/common/uri'; import { basename } from 'vs/base/common/paths'; import { FileKind } from 'vs/platform/files/common/files'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TreeView extends TreeViewsViewletPanel { @@ -92,11 +93,11 @@ export class TreeView extends TreeViewsViewletPanel { const tree = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, container.getHTMLElement(), { dataSource, renderer, controller }, - { keyboardSupport: false } + {} ); tree.contextKeyService.createKey(this.id, true); - this.disposables.push(tree.onDidChangeSelection(() => this.onSelection())); + this.disposables.push(tree.onDidChangeSelection(e => this.onSelection(e))); return tree; } @@ -159,11 +160,17 @@ export class TreeView extends TreeViewsViewletPanel { return DOM.getLargestChildWidth(parentNode, childNodes); } - private onSelection(): void { + private onSelection({ payload }: any): void { const selection: ITreeItem = this.tree.getSelection()[0]; if (selection) { if (selection.command) { - this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); + const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; + const isMouseEvent = payload && payload.origin === 'mouse'; + const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; + + if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); + } } } } @@ -347,15 +354,16 @@ class TreeRenderer implements IRenderer { } } -class TreeController extends DefaultController { +class TreeController extends WorkbenchTreeController { constructor( private treeViewId: string, private menus: Menus, @IContextMenuService private contextMenuService: IContextMenuService, - @IKeybindingService private _keybindingService: IKeybindingService + @IKeybindingService private _keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService ) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false }); + super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false }, configurationService); } public onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 1ab63b6f749d8..0e90dbc877268 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -31,6 +31,7 @@ import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listServic import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; import Event, { Emitter } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface IViewOptions extends IPanelOptions { id: string; @@ -771,9 +772,10 @@ export class FileIconThemableWorkbenchTree extends WorkbenchTree { options: ITreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, - @IThemeService themeService: IWorkbenchThemeService + @IThemeService themeService: IWorkbenchThemeService, + @IConfigurationService configurationService: IConfigurationService ) { - super(container, configuration, { ...options, ...{ showTwistie: false, twistiePixels: 12 } }, contextKeyService, listService, themeService); + super(container, configuration, { ...options, ...{ showTwistie: false, twistiePixels: 12 } }, contextKeyService, listService, themeService, configurationService); DOM.addClass(container, 'file-icon-themable-tree'); DOM.addClass(container, 'show-file-icons'); diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 7b162f1caab07..4b4295d0fa89a 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -380,9 +380,9 @@ export class StackFrame implements IStackFrame { return `${this.name} (${this.source.inMemory ? this.source.name : this.source.uri.fsPath}:${this.range.startLineNumber})`; } - public openInEditor(editorService: IWorkbenchEditorService, preserveFocus?: boolean, sideBySide?: boolean): TPromise { + public openInEditor(editorService: IWorkbenchEditorService, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { return !this.source.available ? TPromise.as(null) : - this.source.openInEditor(editorService, this.range, preserveFocus, sideBySide); + this.source.openInEditor(editorService, this.range, preserveFocus, sideBySide, pinned); } } diff --git a/src/vs/workbench/parts/debug/common/debugSource.ts b/src/vs/workbench/parts/debug/common/debugSource.ts index 22a60851431ad..3816e33cb70ea 100644 --- a/src/vs/workbench/parts/debug/common/debugSource.ts +++ b/src/vs/workbench/parts/debug/common/debugSource.ts @@ -56,7 +56,7 @@ export class Source { return this.uri.scheme === DEBUG_SCHEME; } - public openInEditor(editorService: IWorkbenchEditorService, selection: IRange, preserveFocus?: boolean, sideBySide?: boolean): TPromise { + public openInEditor(editorService: IWorkbenchEditorService, selection: IRange, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { return !this.available ? TPromise.as(null) : editorService.openEditor({ resource: this.uri, description: this.origin, @@ -65,7 +65,7 @@ export class Source { selection, revealIfVisible: true, revealInCenterIfOutsideViewport: true, - pinned: !preserveFocus && !this.inMemory + pinned: pinned || (!preserveFocus && !this.inMemory) } }, sideBySide); } diff --git a/src/vs/workbench/parts/debug/electron-browser/baseDebugView.ts b/src/vs/workbench/parts/debug/electron-browser/baseDebugView.ts index e7e49585dfc7d..5106014521338 100644 --- a/src/vs/workbench/parts/debug/electron-browser/baseDebugView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/baseDebugView.ts @@ -15,11 +15,13 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { once } from 'vs/base/common/functional'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; +import { ClickBehavior, IControllerOptions } from 'vs/base/parts/tree/browser/treeDefaults'; import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -190,19 +192,23 @@ export function renderRenameBox(debugService: IDebugService, contextViewService: })); } -export class BaseDebugController extends DefaultController { +export const DefaultDebugControllerOptions: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false }; + +export class BaseDebugController extends WorkbenchTreeController { private contributedContextMenu: IMenu; constructor( private actionProvider: IActionProvider, menuId: MenuId, + options: IControllerOptions, @IDebugService protected debugService: IDebugService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService menuService: IMenuService + @IMenuService menuService: IMenuService, + @IConfigurationService configurationService: IConfigurationService ) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false }); + super(options, configurationService); this.contributedContextMenu = menuService.createMenu(menuId, contextKeyService); } diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts index 6e4c6862c9145..c6692a98a85fd 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts @@ -78,29 +78,39 @@ export class BreakpointsView extends ViewsViewletPanel { this.list.onContextMenu(this.onListContextMenu, this, this.disposables); - const handleBreakpointFocus = (preserveFocuse: boolean, sideBySide: boolean, selectFunctionBreakpoint: boolean) => { + const handleBreakpointFocus = (preserveFocus: boolean, sideBySide: boolean, selectFunctionBreakpoint: boolean) => { const focused = this.list.getFocusedElements(); const element = focused.length ? focused[0] : undefined; if (element instanceof Breakpoint) { - openBreakpointSource(element, sideBySide, preserveFocuse, this.debugService, this.editorService).done(undefined, onUnexpectedError); + openBreakpointSource(element, sideBySide, preserveFocus, this.debugService, this.editorService).done(undefined, onUnexpectedError); } if (selectFunctionBreakpoint && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedFunctionBreakpoint()) { this.debugService.getViewModel().setSelectedFunctionBreakpoint(element); this.onBreakpointsChange(); } }; + this.disposables.push(this.list.onOpen(e => { + let isSingleClick = false; + let isDoubleClick = false; + let openToSide = false; + + const browserEvent = e.browserEvent; + if (browserEvent instanceof MouseEvent) { + isSingleClick = browserEvent.detail === 1; + isDoubleClick = browserEvent.detail === 2; + openToSide = (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey); + } + + handleBreakpointFocus(isSingleClick, openToSide, isDoubleClick); + })); + + // TODO@Isidor this should be a command (breakpoints.openToSide) this.disposables.push(this.list.onKeyUp(e => { const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter)) { - handleBreakpointFocus(false, event && (event.ctrlKey || event.metaKey || event.altKey), false); + if (event.equals(KeyCode.Enter) && (event.ctrlKey || event.metaKey || event.altKey)) { + handleBreakpointFocus(false, true, false); } })); - this.disposables.push(this.list.onMouseDblClick(e => { - handleBreakpointFocus(false, e.browserEvent.altKey, true); - })); - this.disposables.push(this.list.onMouseClick(e => { - handleBreakpointFocus(true, e.browserEvent.altKey, false); - })); this.list.splice(0, this.list.length, this.elements); } diff --git a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts index 123bcb9be6c56..696f4d33598f4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts @@ -15,7 +15,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { BaseDebugController, twistiePixels, renderViewTree } from 'vs/workbench/parts/debug/electron-browser/baseDebugView'; +import { BaseDebugController, twistiePixels, renderViewTree, DefaultDebugControllerOptions } from 'vs/workbench/parts/debug/electron-browser/baseDebugView'; import { ITree, IActionProvider, IDataSource, IRenderer, IAccessibilityProvider } from 'vs/base/parts/tree/browser/tree'; import { IAction, IActionItem } from 'vs/base/common/actions'; import { RestartAction, StopAction, ContinueAction, StepOverAction, StepIntoAction, StepOutAction, PauseAction, RestartFrameAction } from 'vs/workbench/parts/debug/browser/debugActions'; @@ -24,9 +24,8 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { basenameOrAuthority } from 'vs/base/common/resources'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import ResourceResultsNavigation, { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import FileResultsNavigation from 'vs/workbench/parts/files/browser/fileResultsNavigation'; const $ = dom.$; @@ -93,7 +92,7 @@ export class CallStackView extends TreeViewsViewletPanel { dom.addClass(container, 'debug-call-stack'); this.treeContainer = renderViewTree(container); const actionProvider = new CallStackActionProvider(this.debugService, this.keybindingService); - const controller = this.instantiationService.createInstance(CallStackController, actionProvider, MenuId.DebugCallStackContext); + const controller = this.instantiationService.createInstance(CallStackController, actionProvider, MenuId.DebugCallStackContext, DefaultDebugControllerOptions); this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { dataSource: new CallStackDataSource(), @@ -102,13 +101,12 @@ export class CallStackView extends TreeViewsViewletPanel { controller }, { ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"), - twistiePixels, - keyboardSupport: false + twistiePixels }); - const fileResultsNavigation = new FileResultsNavigation(this.tree); + const fileResultsNavigation = new ResourceResultsNavigation(this.tree); this.disposables.push(fileResultsNavigation); - this.disposables.push(fileResultsNavigation.openFile(e => { + this.disposables.push(fileResultsNavigation.openResource(e => { if (this.ignoreSelectionChangedEvent) { return; } @@ -116,7 +114,7 @@ export class CallStackView extends TreeViewsViewletPanel { const element = e.element; if (element instanceof StackFrame) { this.debugService.focusStackFrame(element, element.thread, element.thread.process, true); - element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide).done(undefined, errors.onUnexpectedError); + element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned).done(undefined, errors.onUnexpectedError); } if (element instanceof Thread) { this.debugService.focusStackFrame(undefined, element, element.process, true); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index 30c4fca449631..a187adff67fdb 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -11,7 +11,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as dom from 'vs/base/browser/dom'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { DefaultController, ICancelableEvent, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; +import { DefaultController, ICancelableEvent, ClickBehavior, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -75,8 +75,7 @@ export class DebugHoverWidget implements IContentWidget { }, { indentPixels: 6, twistiePixels: 15, - ariaLabel: nls.localize('treeAriaLabel', "Debug Hover"), - keyboardSupport: false + ariaLabel: nls.localize('treeAriaLabel', "Debug Hover") }); this.valueContainer = $('.value'); @@ -337,7 +336,7 @@ export class DebugHoverWidget implements IContentWidget { class DebugHoverController extends DefaultController { constructor(private editor: ICodeEditor) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false }); + super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false, openMode: OpenMode.SINGLE_CLICK }); } protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin = 'mouse'): boolean { diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index 6ea2b74afef80..87596c77d68b8 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -44,13 +44,13 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { memoize } from 'vs/base/common/decorators'; import { dispose } from 'vs/base/common/lifecycle'; +import { ClickBehavior, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; const $ = dom.$; const replTreeOptions: ITreeOptions = { twistiePixels: 20, - ariaLabel: nls.localize('replAriaLabel', "Read Eval Print Loop Panel"), - keyboardSupport: false + ariaLabel: nls.localize('replAriaLabel', "Read Eval Print Loop Panel") }; const HISTORY_STORAGE_KEY = 'debug.repl.history'; @@ -135,7 +135,7 @@ export class Repl extends Panel implements IPrivateReplService { this.createReplInput(this.container); this.renderer = this.instantiationService.createInstance(ReplExpressionsRenderer); - const controller = this.instantiationService.createInstance(ReplExpressionsController, new ReplExpressionsActionProvider(this.instantiationService), MenuId.DebugConsoleContext); + const controller = this.instantiationService.createInstance(ReplExpressionsController, new ReplExpressionsActionProvider(this.instantiationService), MenuId.DebugConsoleContext, { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false, openMode: OpenMode.SINGLE_CLICK }); controller.toFocusOnClick = this.replInput; this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { diff --git a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts index e157a61a862e5..8ef474462bd1a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts @@ -28,6 +28,7 @@ import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { ClickBehavior, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; const $ = dom.$; @@ -87,11 +88,10 @@ export class VariablesView extends TreeViewsViewletPanel { dataSource: new VariablesDataSource(), renderer: this.instantiationService.createInstance(VariablesRenderer), accessibilityProvider: new VariablesAccessibilityProvider(), - controller: this.instantiationService.createInstance(VariablesController, new VariablesActionProvider(this.debugService, this.keybindingService), MenuId.DebugVariablesContext) + controller: this.instantiationService.createInstance(VariablesController, new VariablesActionProvider(this.debugService, this.keybindingService), MenuId.DebugVariablesContext, { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false, openMode: OpenMode.SINGLE_CLICK }) }, { ariaLabel: nls.localize('variablesAriaTreeLabel', "Debug Variables"), - twistiePixels, - keyboardSupport: false + twistiePixels }); CONTEXT_VARIABLES_FOCUSED.bindTo(this.tree.contextKeyService); diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index dbbd7bfeb299d..23b519b6472c2 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -26,7 +26,7 @@ import { CopyValueAction } from 'vs/workbench/parts/debug/electron-browser/elect import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { IMouseEvent, DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults'; +import { DefaultDragAndDrop, ClickBehavior, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; import { IVariableTemplateData, renderVariable, renderRenameBox, renderExpressionValue, BaseDebugController, twistiePixels, renderViewTree } from 'vs/workbench/parts/debug/electron-browser/baseDebugView'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; @@ -65,12 +65,11 @@ export class WatchExpressionsView extends TreeViewsViewletPanel { dataSource: new WatchExpressionsDataSource(this.debugService), renderer: this.instantiationService.createInstance(WatchExpressionsRenderer), accessibilityProvider: new WatchExpressionsAccessibilityProvider(), - controller: this.instantiationService.createInstance(WatchExpressionsController, actionProvider, MenuId.DebugWatchContext), + controller: this.instantiationService.createInstance(WatchExpressionsController, actionProvider, MenuId.DebugWatchContext, { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false, openMode: OpenMode.SINGLE_CLICK }), dnd: new WatchExpressionsDragAndDrop(this.debugService) }, { ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"), - twistiePixels, - keyboardSupport: false + twistiePixels }); CONTEXT_WATCH_EXPRESSIONS_FOCUSED.bindTo(this.tree.contextKeyService); diff --git a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts b/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts index b4c3abe5334b2..ae9cd5f371cd6 100644 --- a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts +++ b/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts @@ -9,13 +9,15 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; -import { DefaultController, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; +import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import { Action } from 'vs/base/common/actions'; import { IExtensionDependencies, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { once } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface IExtensionTemplateData { icon: HTMLImageElement; @@ -156,21 +158,24 @@ export class Renderer implements IRenderer { } } -export class Controller extends DefaultController { +export class Controller extends WorkbenchTreeController { - constructor( @IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false }); + constructor( + @IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService, + @IConfigurationService configurationService: IConfigurationService + ) { + super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false }, configurationService); // TODO@Sandeep this should be a command this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); } protected onLeftClick(tree: ITree, element: IExtensionDependencies, event: IMouseEvent): boolean { - let currentFoucssed = tree.getFocus(); + let currentFocused = tree.getFocus(); if (super.onLeftClick(tree, element, event)) { if (element.dependent === null) { - if (currentFoucssed) { - tree.setFocus(currentFoucssed); + if (currentFocused) { + tree.setFocus(currentFocused); } else { tree.focusFirst(); } diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index f101cbd65d6f0..8e6b31e1f5540 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -526,8 +526,7 @@ export class ExtensionEditor extends BaseEditor { controller }, { indentPixels: 40, - twistiePixels: 20, - keyboardSupport: false + twistiePixels: 20 }); tree.setInput(extensionDependencies); diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 84f9afb3ff8d4..a6b981a9006c9 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -1564,10 +1564,10 @@ export class MaliciousStatusLabelAction extends Action { set extension(extension: IExtension) { this._extension = extension; this.update(); } constructor(long: boolean) { - const tooltip = localize('malicious tooltip', "This extension was reported to be malicious."); + const tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); const label = long ? tooltip : localize('malicious', "Malicious"); super('extensions.install', label, '', false); - this.tooltip = localize('malicious tooltip', "This extension was reported to be malicious."); + this.tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); } private update(): void { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 59a354f9f39bc..7c51274b7ff67 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -486,7 +486,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { if (maliciousExtensions.length) { return TPromise.join(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e, true).then(() => { this.messageService.show(Severity.Warning, { - message: localize('malicious warning', "We have uninstalled '{0}' which was reported to be malicious.", getGalleryExtensionIdFromLocal(e)), + message: localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", getGalleryExtensionIdFromLocal(e)), actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, localize('reloadNow', "Reload Now"))] }); }))); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index 69e6766f25c2d..c3806ceef8da4 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -78,11 +78,10 @@ export class ExtensionsListView extends ViewsViewletPanel { const delegate = new Delegate(); const renderer = this.instantiationService.createInstance(Renderer); this.list = this.instantiationService.createInstance(WorkbenchPagedList, this.extensionsList, delegate, [renderer], { - ariaLabel: localize('extensions', "Extensions"), - keyboardSupport: false + ariaLabel: localize('extensions', "Extensions") }); - chain(this.list.onSelectionChange) + chain(this.list.onOpen) .map(e => e.elements[0]) .filter(e => !!e) .on(this.openExtension, this, this.disposables); @@ -443,6 +442,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const activeEditorInput = this.editorService.getActiveEditorInput(); this.editorInputService.pinEditor(activeEditor.position, activeEditorInput); + activeEditor.focus(); } diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 23944c6caadaf..5faf66742fe19 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -569,7 +569,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } if (extension.isMalicious) { - return TPromise.wrapError(new Error(nls.localize('malicious', "This extension is reported to be malicious."))); + return TPromise.wrapError(new Error(nls.localize('malicious', "This extension is reported to be problematic."))); } const ext = extension as Extension; @@ -581,7 +581,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { return this.progressService.withProgress({ location: ProgressLocation.Extensions, - title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Market place....'), + title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Marketplace....'), tooltip: `${extension.id}` }, () => this.extensionService.installFromGallery(gallery)); } diff --git a/src/vs/workbench/parts/files/browser/fileResultsNavigation.ts b/src/vs/workbench/parts/files/browser/fileResultsNavigation.ts deleted file mode 100644 index 46558d347a045..0000000000000 --- a/src/vs/workbench/parts/files/browser/fileResultsNavigation.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import Event, { Emitter } from 'vs/base/common/event'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { ITree } from 'vs/base/parts/tree/browser/tree'; - -export interface IOpenFileOptions { - editorOptions: IEditorOptions; - sideBySide: boolean; - element: any; - payload: any; -} - -export interface IFileResultsNavigationOptions { - openOnFocus: boolean; -} - -export default class FileResultsNavigation extends Disposable { - - private _openFile: Emitter = new Emitter(); - public readonly openFile: Event = this._openFile.event; - - constructor(private tree: ITree, options?: IFileResultsNavigationOptions) { - super(); - if (options && options.openOnFocus) { - this._register(this.tree.onDidChangeFocus(e => this.onFocus(e))); - } - this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); - } - - private onFocus(event: any): void { - const element = this.tree.getFocus(); - this.tree.setSelection([element], { fromFocus: true }); - this._openFile.fire({ - editorOptions: { - preserveFocus: true, - pinned: false, - revealIfVisible: true - }, - sideBySide: false, - element, - payload: event.payload - }); - } - - private onSelection({ payload }: any): void { - if (payload && payload.fromFocus) { - return; - } - - const keyboard = payload && payload.origin === 'keyboard'; - const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; - - const pinned = (payload && payload.origin === 'mouse' && originalEvent && originalEvent.detail === 2); - if (pinned && originalEvent) { - originalEvent.preventDefault(); // focus moves to editor, we need to prevent default - } - - const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey || originalEvent.altKey)); - const preserveFocus = !((keyboard && (!payload || !payload.preserveFocus)) || pinned || (payload && payload.focusEditor)); - this._openFile.fire({ - editorOptions: { - preserveFocus, - pinned, - revealIfVisible: true - }, - sideBySide, - element: this.tree.getSelection()[0], - payload - }); - } -} diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index 09a9d91152467..10fa67ffe8e49 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -12,6 +12,7 @@ import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerMo import { toResource } from 'vs/workbench/common/editor'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { List } from 'vs/base/browser/ui/list/listWidget'; +import { IFileStat } from 'vs/platform/files/common/files'; // Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding // To cover all these cases we need to properly compute the resource on which the command is being executed @@ -38,18 +39,26 @@ export function getMultiSelectedResources(resource: URI, listService: IListServi if (list && list.isDOMFocused()) { // Explorer if (list instanceof Tree) { - const focus = list.getFocus(); - const selection = list.getSelection(); - if (selection && selection.indexOf(focus) >= 0) { - return selection.map(fs => fs.resource); + const focus: IFileStat = list.getFocus(); + // If the resource is passed it has to be a part of the returned context. + if (focus && (!URI.isUri(resource) || focus.resource.toString() === resource.toString())) { + const selection = list.getSelection(); + // We only respect the selection if it contains the focused element. + if (selection && selection.indexOf(focus) >= 0) { + return selection.map(fs => fs.resource); + } } } // Open editors view if (list instanceof List) { const focus = list.getFocusedElements(); - const selection = list.getSelectedElements(); - if (selection && focus.length && selection.indexOf(focus[0]) >= 0) { - return selection.filter(s => s instanceof OpenEditor).map((oe: OpenEditor) => oe.getResource()); + // If the resource is passed it has to be a part of the returned context. + if (focus.length && (!URI.isUri(resource) || (focus[0] instanceof OpenEditor && focus[0].getResource().toString() === resource.toString()))) { + const selection = list.getSelectedElements(); + // We only respect the selection if it contains the focused element. + if (selection && selection.indexOf(focus[0]) >= 0) { + return selection.filter(s => s instanceof OpenEditor).map((oe: OpenEditor) => oe.getResource()); + } } } } diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index ed348ccc0888f..621a89dba0332 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -380,6 +380,10 @@ export class OpenEditor implements IEditorIdentifier { return this._editor; } + public get editorIndex() { + return this._group.indexOf(this.editor); + } + public get group() { return this._group; } diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index b562912f18126..5da55c98057e7 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -12,7 +12,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as labels from 'vs/base/common/labels'; import URI from 'vs/base/common/uri'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource, IEditorIdentifier } from 'vs/workbench/common/editor'; +import { toResource, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -492,8 +492,9 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: SAVE_ALL_IN_GROUP_COMMAND_ID, - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, editorContext: IEditorCommandsContext) => { const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService)); + const editorGroupService = accessor.get(IEditorGroupService); let saveAllArg: any; if (!contexts.length) { saveAllArg = true; @@ -501,7 +502,7 @@ CommandsRegistry.registerCommand({ const fileService = accessor.get(IFileService); saveAllArg = []; contexts.forEach(context => { - const editorGroup = context.group; + const editorGroup = editorGroupService.getStacksModel().getGroup(context.groupId); editorGroup.getEditors().forEach(editor => { const resource = toResource(editor, { supportSideBySide: true }); if (resource && (resource.scheme === 'untitled' || fileService.canHandleResource(resource))) { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 9d28ff82fa45e..d60b9e72bcbb3 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -403,8 +403,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView accessibilityProvider }, { autoExpandSingleChildren: true, - ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), - keyboardSupport: false + ariaLabel: nls.localize('treeAriaLabel', "Files Explorer") }); // Bind context keys diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index b5f34511a478d..eaf035bc91e3a 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -29,7 +29,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData, SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd'; -import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; +import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import { FileStat, NewStatPlaceholder, Model } from 'vs/workbench/parts/files/common/explorerModel'; import { DragMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -56,7 +56,7 @@ import { extractResources } from 'vs/workbench/browser/editor'; import { relative } from 'path'; import { DataTransfers } from 'vs/base/browser/dnd'; import { distinctParents } from 'vs/base/common/resources'; -import { WorkbenchTree, multiSelectModifierSettingKey } from 'vs/platform/list/browser/listService'; +import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; export class FileDataSource implements IDataSource { constructor( @@ -325,12 +325,11 @@ export class FileAccessibilityProvider implements IAccessibilityProvider { } // Explorer Controller -export class FileController extends DefaultController implements IDisposable { +export class FileController extends WorkbenchTreeController implements IDisposable { private contributedContextMenu: IMenu; private toDispose: IDisposable[]; private previousSelectionRangeStop: FileStat; - private useAltAsMultiSelectModifier: boolean; constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @@ -338,25 +337,14 @@ export class FileController extends DefaultController implements IDisposable { @ITelemetryService private telemetryService: ITelemetryService, @IMenuService private menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService ) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false /* handled via IListService */ }); + super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false /* handled via IListService */ }, configurationService); - this.useAltAsMultiSelectModifier = configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; this.toDispose = []; - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this.useAltAsMultiSelectModifier = this.configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; - } - })); } - public onLeftClick(tree: ITree, stat: FileStat | Model, event: IMouseEvent, origin: string = 'mouse'): boolean { + public onLeftClick(tree: WorkbenchTree, stat: FileStat | Model, event: IMouseEvent, origin: string = 'mouse'): boolean { const payload = { origin: origin }; const isDoubleClick = (origin === 'mouse' && event.detail === 2); @@ -394,7 +382,7 @@ export class FileController extends DefaultController implements IDisposable { } // Allow to multiselect - if ((this.useAltAsMultiSelectModifier && event.altKey) || !this.useAltAsMultiSelectModifier && (event.ctrlKey || event.metaKey)) { + if ((tree.useAltAsMultipleSelectionModifier && event.altKey) || !tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey)) { const selection = tree.getSelection(); this.previousSelectionRangeStop = undefined; if (selection.indexOf(stat) >= 0) { @@ -419,9 +407,12 @@ export class FileController extends DefaultController implements IDisposable { // Select, Focus and open files else { + // Expand / Collapse - tree.toggleExpansion(stat, event.altKey); - this.previousSelectionRangeStop = undefined; + if (isDoubleClick || this.openOnSingleClick) { + tree.toggleExpansion(stat, event.altKey); + this.previousSelectionRangeStop = undefined; + } const preserveFocus = !isDoubleClick; tree.setFocus(stat, payload); @@ -432,10 +423,10 @@ export class FileController extends DefaultController implements IDisposable { tree.setSelection([stat], payload); - if (!stat.isDirectory) { + if (!stat.isDirectory && (isDoubleClick || this.openOnSingleClick)) { let sideBySide = false; if (event) { - sideBySide = this.useAltAsMultiSelectModifier ? (event.ctrlKey || event.metaKey) : event.altKey; + sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; } this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick }); diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index d411f925e0916..92fea9e8412cf 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -28,15 +28,13 @@ import { EditorGroup } from 'vs/workbench/common/editor/editorStacksModel'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { WorkbenchList, useAltAsMultipleSelectionModifier } from 'vs/platform/list/browser/listService'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { EditorLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; @@ -153,7 +151,6 @@ export class OpenEditorsView extends ViewsViewletPanel { new EditorGroupRenderer(this.keybindingService, this.instantiationService, this.editorGroupService), new OpenEditorRenderer(getSelectedElements, this.instantiationService, this.keybindingService, this.configurationService, this.editorGroupService) ], { - keyboardSupport: false, identityProvider: element => element instanceof OpenEditor ? element.getId() : element.id.toString() }); @@ -185,15 +182,28 @@ export class OpenEditorsView extends ViewsViewletPanel { }); // Open when selecting via keyboard - this.disposables.push(this.list.onMouseClick(e => this.onMouseClick(e, false))); - this.disposables.push(this.list.onMouseDblClick(e => this.onMouseClick(e, true))); - this.disposables.push(this.list.onKeyDown(e => { - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.Enter) { - const focused = this.list.getFocusedElements(); - const element = focused.length ? focused[0] : undefined; - if (element instanceof OpenEditor) { - this.openEditor(element, { pinned: false, sideBySide: !!(event.altKey || event.ctrlKey || event.metaKey), preserveFocus: false }); + this.disposables.push(this.list.onOpen(e => { + const browserEvent = e.browserEvent; + + let openToSide = false; + let isSingleClick = false; + let isDoubleClick = false; + let isMiddleClick = false; + if (browserEvent instanceof MouseEvent) { + isSingleClick = browserEvent.detail === 1; + isDoubleClick = browserEvent.detail === 2; + isMiddleClick = browserEvent.button === 1 /* middle button */; + openToSide = this.list.useAltAsMultipleSelectionModifier ? (browserEvent.ctrlKey || browserEvent.metaKey) : browserEvent.altKey; + } + + const focused = this.list.getFocusedElements(); + const element = focused.length ? focused[0] : undefined; + if (element instanceof OpenEditor) { + if (isMiddleClick) { + const position = this.model.positionOfGroup(element.group); + this.editorService.closeEditor(position, element.editor).done(null, errors.onUnexpectedError); + } else { + this.openEditor(element, { preserveFocus: isSingleClick, pinned: isDoubleClick, sideBySide: openToSide }); } } })); @@ -268,21 +278,6 @@ export class OpenEditorsView extends ViewsViewletPanel { return -1; } - private onMouseClick(event: IListMouseEvent, isDoubleClick: boolean): void { - const element = event.element; - if (!(element instanceof OpenEditor)) { - return; - } - - if (event.browserEvent && event.browserEvent.button === 1 /* Middle Button */) { - const position = this.model.positionOfGroup(element.group); - this.editorService.closeEditor(position, element.editor).done(null, errors.onUnexpectedError); - } else { - const sideBySide = useAltAsMultipleSelectionModifier(this.configurationService) ? event.browserEvent.altKey : (event.browserEvent.ctrlKey || event.browserEvent.metaKey); - this.openEditor(element, { preserveFocus: !isDoubleClick, pinned: isDoubleClick, sideBySide }); - } - } - private openEditor(element: OpenEditor, options: { preserveFocus: boolean; pinned: boolean; sideBySide: boolean; }): void { if (element) { /* __GDPR__ @@ -311,7 +306,7 @@ export class OpenEditorsView extends ViewsViewletPanel { fillInActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? element.editor.getResource() : {} }, actions, this.contextMenuService); return TPromise.as(actions); }, - getActionsContext: () => element instanceof OpenEditor ? { group: element.group, editor: element.editor } : { group: element } + getActionsContext: () => element instanceof OpenEditor ? { groupId: element.group.id, editorIndex: element.editorIndex } : { groupId: element.id } }); } @@ -493,7 +488,7 @@ class EditorGroupRenderer implements IRenderer().explorer.decorations }); - templateData.actionBar.context = { group: editor.group, editor: editor.editor }; + templateData.actionBar.context = { groupId: editor.group.id, editorIndex: editor.editorIndex }; } disposeTemplate(templateData: IOpenEditorTemplateData): void { diff --git a/src/vs/workbench/parts/markers/browser/markersPanel.ts b/src/vs/workbench/parts/markers/browser/markersPanel.ts index 6cd0676b79cc8..aa2b09d7da8f2 100644 --- a/src/vs/workbench/parts/markers/browser/markersPanel.ts +++ b/src/vs/workbench/parts/markers/browser/markersPanel.ts @@ -27,11 +27,10 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import Messages from 'vs/workbench/parts/markers/common/messages'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import FileResultsNavigation from 'vs/workbench/parts/files/browser/fileResultsNavigation'; import { debounceEvent } from 'vs/base/common/event'; import { SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import ResourceResultsNavigation, { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IMarkersWorkbenchService } from 'vs/workbench/parts/markers/common/markers'; export class MarkersPanel extends Panel { @@ -206,14 +205,13 @@ export class MarkersPanel extends Panel { }, { indentPixels: 0, twistiePixels: 20, - ariaLabel: Messages.MARKERS_PANEL_ARIA_LABEL_PROBLEMS_TREE, - keyboardSupport: false + ariaLabel: Messages.MARKERS_PANEL_ARIA_LABEL_PROBLEMS_TREE }); Constants.MarkerFocusContextKey.bindTo(this.tree.contextKeyService); - const fileResultsNavigation = this._register(new FileResultsNavigation(this.tree, { openOnFocus: true })); - this._register(debounceEvent(fileResultsNavigation.openFile, (last, event) => event, 75, true)(options => { + const fileResultsNavigation = this._register(new ResourceResultsNavigation(this.tree, { openOnFocus: true })); + this._register(debounceEvent(fileResultsNavigation.openResource, (last, event) => event, 75, true)(options => { this.openFileAtElement(options.element, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned); })); } diff --git a/src/vs/workbench/parts/markers/browser/markersTreeController.ts b/src/vs/workbench/parts/markers/browser/markersTreeController.ts index 07e688331c858..a460210a9233b 100644 --- a/src/vs/workbench/parts/markers/browser/markersTreeController.ts +++ b/src/vs/workbench/parts/markers/browser/markersTreeController.ts @@ -14,16 +14,18 @@ import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -export class Controller extends treedefaults.DefaultController { +export class Controller extends WorkbenchTreeController { constructor( @IContextMenuService private contextMenuService: IContextMenuService, @IMenuService private menuService: IMenuService, - @IKeybindingService private _keybindingService: IKeybindingService + @IKeybindingService private _keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService ) { - super({ clickBehavior: treedefaults.ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: false }); + super({ clickBehavior: treedefaults.ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: false }, configurationService); } protected onLeftClick(tree: tree.ITree, element: any, event: mouse.IMouseEvent): boolean { diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts index 4171d03edc2c3..3fd4c0eac5e96 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -333,7 +333,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.keybindingsListContainer = DOM.append(parent, $('.keybindings-list-container')); this.keybindingsList = this._register(this.instantiationService.createInstance(WorkbenchList, this.keybindingsListContainer, new Delegate(), [new KeybindingHeaderRenderer(), new KeybindingItemRenderer(this, this.keybindingsService)], - { identityProvider: e => e.id, keyboardSupport: false, mouseSupport: true, ariaLabel: localize('keybindingsLabel', "Keybindings") })); + { identityProvider: e => e.id, mouseSupport: true, ariaLabel: localize('keybindingsLabel', "Keybindings") })); this._register(this.keybindingsList.onContextMenu(e => this.onContextMenu(e))); this._register(this.keybindingsList.onFocusChange(e => this.onFocusChange(e))); this._register(this.keybindingsList.onDidFocus(() => { diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index dfdeb0359ed47..9cfcc4ed51b32 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -854,8 +854,7 @@ export class RepositoryPanel extends ViewletPanel { ]; this.list = this.instantiationService.createInstance(WorkbenchList, this.listContainer, delegate, renderers, { - identityProvider: scmResourceIdentityProvider, - keyboardSupport: false + identityProvider: scmResourceIdentityProvider }); chain(this.list.onOpen) @@ -941,6 +940,7 @@ export class RepositoryPanel extends ViewletPanel { } this.editorGroupService.pinEditor(activeEditor.position, activeEditorInput); + activeEditor.focus(); } private onListContextMenu(e: IListContextMenuEvent): void { diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index 333c9d7dc94a3..25cf4bc34b61f 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -53,12 +53,11 @@ import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/act import * as Constants from 'vs/workbench/parts/search/common/constants'; import { IThemeService, ITheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorFindMatchHighlight, diffInserted, diffRemoved, diffInsertedOutline, diffRemovedOutline, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import FileResultsNavigation from 'vs/workbench/parts/files/browser/fileResultsNavigation'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/parts/search/common/search'; import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd'; import { isDiffEditor, isCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import ResourceResultsNavigation, { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; export class SearchViewlet extends Viewlet { @@ -84,7 +83,7 @@ export class SearchViewlet extends Viewlet { private searching: boolean; private actions: SearchAction[] = []; - private tree: ITree; + private tree: WorkbenchTree; private viewletSettings: any; private messages: Builder; private searchWidgetsContainer: Builder; @@ -505,15 +504,14 @@ export class SearchViewlet extends Viewlet { accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider), dnd }, { - ariaLabel: nls.localize('treeAriaLabel', "Search Results"), - keyboardSupport: false + ariaLabel: nls.localize('treeAriaLabel', "Search Results") }); this.tree.setInput(this.viewModel.searchResult); this.toUnbind.push(renderer); - const fileResultsNavigation = this._register(new FileResultsNavigation(this.tree, { openOnFocus: true })); - this._register(debounceEvent(fileResultsNavigation.openFile, (last, event) => event, 75, true)(options => { + const fileResultsNavigation = this._register(new ResourceResultsNavigation(this.tree, { openOnFocus: true })); + this._register(debounceEvent(fileResultsNavigation.openResource, (last, event) => event, 75, true)(options => { if (options.element instanceof Match) { let selectedMatch: Match = options.element; if (this.currentSelectedFileMatch) { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts index 1ce6fdca2c261..d79f768e15018 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts @@ -289,6 +289,25 @@ export class ExtensionScannerInput { } } +export interface IExtensionReference { + name: string; + path: string; +} + +export interface IExtensionResolver { + resolveExtensions(): TPromise; +} + +class DefaultExtensionResolver implements IExtensionResolver { + + constructor(private root: string) { } + + resolveExtensions(): TPromise { + return pfs.readDirsInDir(this.root) + .then(folders => folders.map(name => ({ name, path: join(this.root, name) }))); + } +} + export class ExtensionScanner { /** @@ -318,10 +337,14 @@ export class ExtensionScanner { /** * Scan a list of extensions defined in `absoluteFolderPath` */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog): TPromise { + public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver?: IExtensionResolver): TPromise { const absoluteFolderPath = input.absoluteFolderPath; const isBuiltin = input.isBuiltin; + if (!resolver) { + resolver = new DefaultExtensionResolver(absoluteFolderPath); + } + try { let obsolete: { [folderName: string]: boolean; } = {}; if (!isBuiltin) { @@ -333,38 +356,35 @@ export class ExtensionScanner { } } - const rawFolders = await pfs.readDirsInDir(absoluteFolderPath); + let refs = await resolver.resolveExtensions(); // Ensure the same extension order - rawFolders.sort(); + refs.sort((a, b) => a.name < b.name ? -1 : 1); - let folders: string[] = null; - if (isBuiltin) { - folders = rawFolders; - } else { + if (!isBuiltin) { // TODO: align with extensionsService - const nonGallery: string[] = []; - const gallery: string[] = []; + const nonGallery: IExtensionReference[] = []; + const gallery: IExtensionReference[] = []; - rawFolders.forEach(folder => { - if (obsolete[folder]) { + refs.forEach(ref => { + if (obsolete[ref.name]) { return; } - const { id, version } = getIdAndVersionFromLocalExtensionId(folder); + const { id, version } = getIdAndVersionFromLocalExtensionId(ref.name); if (!id || !version) { - nonGallery.push(folder); + nonGallery.push(ref); } else { - gallery.push(folder); + gallery.push(ref); } }); - folders = [...nonGallery, ...gallery]; + refs = [...nonGallery, ...gallery]; } const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let extensionDescriptions = await TPromise.join(folders.map(f => this.scanExtension(input.ourVersion, log, join(absoluteFolderPath, f), isBuiltin, nlsConfig))); + let extensionDescriptions = await TPromise.join(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, nlsConfig))); extensionDescriptions = extensionDescriptions.filter(item => item !== null); if (!isBuiltin) { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 1f32a1e5386bb..020e3c73d3076 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -19,7 +19,7 @@ import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, import { IExtensionEnablementService, IExtensionIdentifier, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/platform/extensions/common/extensionsRegistry'; -import { ExtensionScanner, ILog, ExtensionScannerInput } from 'vs/workbench/services/extensions/electron-browser/extensionPoints'; +import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/electron-browser/extensionPoints'; import { IMessageService, CloseAction } from 'vs/platform/message/common/message'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol'; @@ -46,6 +46,42 @@ import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); const ExtraDevSystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', '.build', 'builtInExtensions')); +interface IBuiltInExtension { + name: string; + version: string; + repo: string; +} + +interface IBuiltInExtensionControl { + [name: string]: 'marketplace' | 'disabled' | string; +} + +class ExtraBuiltInExtensionResolver implements IExtensionResolver { + + constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { } + + resolveExtensions(): TPromise { + const result: IExtensionReference[] = []; + + for (const ext of this.builtInExtensions) { + const controlState = this.control[ext.name] || 'marketplace'; + + switch (controlState) { + case 'disabled': + break; + case 'marketplace': + result.push({ name: ext.name, path: path.join(ExtraDevSystemExtensionsRoot, ext.name) }); + break; + default: + result.push({ name: ext.name, path: controlState }); + break; + } + } + + return TPromise.as(result); + } +} + // Enable to see detailed message communication between window and extension host const logExtensionHostCommunication = false; @@ -632,7 +668,19 @@ export class ExtensionService extends Disposable implements IExtensionService { let finalBuiltinExtensions: TPromise = builtinExtensions; if (devMode) { - const extraBuiltinExtensions = ExtensionScanner.scanExtensions(new ExtensionScannerInput(version, commit, locale, devMode, ExtraDevSystemExtensionsRoot, true), log); + const builtInExtensionsFilePath = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'build', 'builtInExtensions.json')); + const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8') + .then(raw => JSON.parse(raw)); + + const controlFilePath = path.join(process.env['HOME'], '.vscode-oss-dev', 'extensions', 'control.json'); + const controlFile = pfs.readFile(controlFilePath, 'utf8') + .then(raw => JSON.parse(raw), () => ({} as any)); + + const input = new ExtensionScannerInput(version, commit, locale, devMode, ExtraDevSystemExtensionsRoot, true); + const extraBuiltinExtensions = TPromise.join([builtInExtensions, controlFile]) + .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) + .then(resolver => ExtensionScanner.scanExtensions(input, log, resolver)); + finalBuiltinExtensions = TPromise.join([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => { let resultMap: { [id: string]: IExtensionDescription; } = Object.create(null); for (let i = 0, len = builtinExtensions.length; i < len; i++) {