diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 34c4061840..c2dbe0f13e 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -103,6 +103,7 @@ export class AzureActiveDirectoryService { await Promise.all(refreshes); } catch (e) { + Logger.info('Failed to initialize stored data'); await this.clearSessions(); } } @@ -170,6 +171,7 @@ export class AzureActiveDirectoryService { if (this._tokens.length) { // Log out all removedIds = this._tokens.map(token => token.sessionId); + Logger.info('No tokens in memory, clearing keychain data'); await this.clearSessions(); } } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index c4d7817b82..24f6cba29a 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?fb4c14f317e1decb0289895ecc9356f0") format("truetype"); + src: url("./codicon.ttf?3d9ee7d873425ff0bc441f48a1de0c54") format("truetype"); } .codicon[class*='codicon-'] { @@ -420,4 +420,7 @@ .codicon-bell-dot:before { content: "\f102" } .codicon-debug-alt-2:before { content: "\f103" } .codicon-debug-alt:before { content: "\f104" } -.codicon-run-all:before { content: "\f105" } +.codicon-debug-console:before { content: "\f105" } +.codicon-library:before { content: "\f106" } +.codicon-output:before { content: "\f107" } +.codicon-run-all:before { content: "\f108" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 2c2e05844c..a3c15e7e4b 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 2caa6c2b34..379f8a0fcf 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -275,6 +275,12 @@ export class ListView implements ISpliceable, IDisposable { this.layout(); } + updateOptions(options: IListViewOptions) { + if (options.additionalScrollHeight !== undefined) { + this.additionalScrollHeight = options.additionalScrollHeight; + } + } + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent); } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 0a4dd4dc09..6c6b7319f7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -854,6 +854,7 @@ export interface IListOptions { readonly mouseSupport?: boolean; readonly horizontalScrolling?: boolean; readonly ariaProvider?: IAriaProvider; + readonly additionalScrollHeight?: number; } export interface IListStyles { @@ -1110,6 +1111,7 @@ class ListViewDragAndDrop implements IListViewDragAndDrop { export interface IListOptionsUpdate { readonly enableKeyboardNavigation?: boolean; readonly automaticKeyboardNavigation?: boolean; + readonly additionalScrollHeight?: number; } export class List implements ISpliceable, IDisposable { @@ -1289,6 +1291,10 @@ export class List implements ISpliceable, IDisposable { if (this.typeLabelController) { this.typeLabelController.updateOptions(this._options); } + + if (optionsUpdate.additionalScrollHeight !== undefined) { + this.view.updateOptions(optionsUpdate); + } } get options(): IListOptions { diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 569314ea66..43c1387fae 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -110,7 +110,7 @@ abstract class ViewItem { get snap(): boolean { return !!this.view.snap; } set enabled(enabled: boolean) { - this.container.style.pointerEvents = enabled ? null : 'none'; + this.container.style.pointerEvents = enabled ? '' : 'none'; } constructor( diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 341a02d3b0..d60eab1c22 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -791,7 +791,7 @@ export class AsyncDataTree implements IDisposable this.refreshPromises.set(node, result); - return result.finally(() => this.refreshPromises.delete(node)); + return result.finally(() => { this.refreshPromises.delete(node); }); } private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent | null, any>): void { diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index fbf2fd8987..1f3e6d022d 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -28,11 +28,17 @@ export class HistoryNavigator implements INavigator { } public next(): T | null { - return this._navigator.next(); + if (this._currentPosition() !== this._elements.length - 1) { + return this._navigator.next(); + } + return null; } public previous(): T | null { - return this._navigator.previous(); + if (this._currentPosition() !== 0) { + return this._navigator.previous(); + } + return null; } public current(): T | null { @@ -73,6 +79,15 @@ export class HistoryNavigator implements INavigator { } } + private _currentPosition(): number { + const currentElement = this._navigator.current(); + if (!currentElement) { + return -1; + } + + return this._elements.indexOf(currentElement); + } + private _initialize(history: readonly T[]): void { this._history = new Set(); for (const entry of history) { diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index c14dec88b9..730d88acd9 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -5,7 +5,7 @@ import * as extpath from 'vs/base/common/extpath'; import * as paths from 'vs/base/common/path'; -import { URI, originalFSPath as uriOriginalFSPath } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; import { isLinux, isWindows } from 'vs/base/common/platform'; @@ -13,7 +13,28 @@ import { CharCode } from 'vs/base/common/charCode'; import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { TernarySearchTree } from 'vs/base/common/map'; -export const originalFSPath = uriOriginalFSPath; +export function originalFSPath(uri: URI): string { + let value: string; + const uriPath = uri.path; + if (uri.authority && uriPath.length > 1 && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uriPath}`; + } else if ( + isWindows + && uriPath.charCodeAt(0) === CharCode.Slash + && extpath.isWindowsDriveLetter(uriPath.charCodeAt(1)) + && uriPath.charCodeAt(2) === CharCode.Colon + ) { + value = uriPath.substr(1); + } else { + // other path + value = uriPath; + } + if (isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; +} /** * Creates a key from a resource URI to be used to resource comparison and for resource maps. @@ -24,7 +45,7 @@ export function getComparisonKey(resource: URI, caseInsensitivePath = hasToIgnor if (caseInsensitivePath) { path = path.toLowerCase(); } - return `${resource.scheme}://${resource.authority.toLowerCase()}/${path}?${resource.query}`; + return resource.with({ authority: resource.authority.toLowerCase(), path: path, fragment: null }).toString(); } export function hasToIgnoreCase(resource: URI | undefined): boolean { @@ -123,7 +144,15 @@ export function dirname(resource: URI): URI { * @returns The resulting URI. */ export function joinPath(resource: URI, ...pathFragment: string[]): URI { - return URI.joinPaths(resource, ...pathFragment); + let joinedPath: string; + if (resource.scheme === 'file') { + joinedPath = URI.file(paths.join(originalFSPath(resource), ...pathFragment)).path; + } else { + joinedPath = paths.posix.join(resource.path || '/', ...pathFragment); + } + return resource.with({ + path: joinedPath + }); } /** diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 92eb50a131..0cc0f0081a 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -6,7 +6,6 @@ import { isWindows } from 'vs/base/common/platform'; import { CharCode } from 'vs/base/common/charCode'; import * as paths from 'vs/base/common/path'; -import * as extpath from 'vs/base/common/extpath'; const _schemePattern = /^\w[\w\d+.-]*$/; const _singleSlashStart = /^\//; @@ -206,7 +205,7 @@ export class URI implements UriComponents { // if (this.scheme !== 'file') { // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); // } - return _makeFsPath(this); + return _makeFsPath(this, false); } // ---- modify to new ------------------------- @@ -340,20 +339,21 @@ export class URI implements UriComponents { /** * Join a URI path with path fragments and normalizes the resulting path. * - * @param resource The input URI. + * @param uri The input URI. * @param pathFragment The path fragment to add to the URI path. * @returns The resulting URI. */ - static joinPaths(resource: URI, ...pathFragment: string[]): URI { - let joinedPath: string; - if (resource.scheme === 'file') { - joinedPath = URI.file(paths.join(originalFSPath(resource), ...pathFragment)).path; - } else { - joinedPath = paths.posix.join(resource.path || '/', ...pathFragment); + static joinPath(uri: URI, ...pathFragment: string[]): URI { + if (!uri.path) { + throw new Error(`[UriError]: cannot call joinPaths on URI without path`); } - return resource.with({ - path: joinedPath - }); + let newPath: string; + if (isWindows && uri.scheme === 'file') { + newPath = URI.file(paths.win32.join(_makeFsPath(uri, true), ...pathFragment)).path; + } else { + newPath = paths.posix.join(uri.path, ...pathFragment); + } + return uri.with({ path: newPath }); } // ---- printing/externalize --------------------------- @@ -421,7 +421,7 @@ class _URI extends URI { get fsPath(): string { if (!this._fsPath) { - this._fsPath = _makeFsPath(this); + this._fsPath = _makeFsPath(this, false); } return this._fsPath; } @@ -577,7 +577,7 @@ function encodeURIComponentMinimal(path: string): string { /** * Compute `fsPath` for the given uri */ -function _makeFsPath(uri: URI): string { +function _makeFsPath(uri: URI, keepDriveLetterCasing: boolean): string { let value: string; if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { @@ -589,7 +589,11 @@ function _makeFsPath(uri: URI): string { && uri.path.charCodeAt(2) === CharCode.Colon ) { // windows drive letter: file:///c:/far/boo - value = uri.path[1].toLowerCase() + uri.path.substr(2); + if (!keepDriveLetterCasing) { + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + value = uri.path.substr(1, 2); + } } else { // other path value = uri.path; @@ -695,29 +699,3 @@ function percentDecode(str: string): string { } return str.replace(_rEncodedAsHex, (match) => decodeURIComponentGraceful(match)); } - - -// --- utils - -export function originalFSPath(uri: URI): string { - let value: string; - const uriPath = uri.path; - if (uri.authority && uriPath.length > 1 && uri.scheme === 'file') { - // unc path: file://shares/c$/far/boo - value = `//${uri.authority}${uriPath}`; - } else if ( - isWindows - && uriPath.charCodeAt(0) === CharCode.Slash - && extpath.isWindowsDriveLetter(uriPath.charCodeAt(1)) - && uriPath.charCodeAt(2) === CharCode.Colon - ) { - value = uriPath.substr(1); - } else { - // other path - value = uriPath; - } - if (isWindows) { - value = value.replace(/\//g, '\\'); - } - return value; -} diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index b489412021..ad98ceb8d1 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -526,7 +526,7 @@ export class ChannelClient implements IChannelClient, IDisposable { this.activeRequests.add(disposable); }); - return result.finally(() => this.activeRequests.delete(disposable)); + return result.finally(() => { this.activeRequests.delete(disposable); }); } private requestEvent(channelName: string, name: string, arg?: any): Event { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index bf4e4d21d3..65ecf5b815 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS, ItemActivation } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList, QuickInputListFocus } from './quickInputList'; @@ -20,11 +20,9 @@ import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; -import { URI } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils'; -import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { List, IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -70,10 +68,7 @@ const $ = dom.$; type Writeable = { -readonly [P in keyof T]: T[P] }; const backButton = { - iconPath: { - dark: URI.parse(registerAndGetAmdImageURL('vs/base/parts/quickinput/browser/media/arrow-left-dark.svg')), - light: URI.parse(registerAndGetAmdImageURL('vs/base/parts/quickinput/browser/media/arrow-left-light.svg')) - }, + iconClass: 'codicon-arrow-left', tooltip: localize('quickInput.back', "Back"), handle: -1 // TODO }; @@ -391,7 +386,7 @@ class QuickPick extends QuickInput implements IQuickPi private _matchOnLabel = true; private _sortByLabel = true; private _autoFocusOnList = true; - private _autoFocusSecondEntry = false; + private _itemActivation = ItemActivation.FIRST; private _activeItems: T[] = []; private activeItemsUpdated = false; private activeItemsToConfirm: T[] | null = []; @@ -527,12 +522,12 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } - get autoFocusSecondEntry() { - return this._autoFocusSecondEntry; + get itemActivation() { + return this._itemActivation; } - set autoFocusSecondEntry(autoFocusSecondEntry: boolean) { - this._autoFocusSecondEntry = autoFocusSecondEntry; + set itemActivation(itemActivation: ItemActivation) { + this._itemActivation = itemActivation; } get activeItems() { @@ -879,11 +874,18 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); - if (this._autoFocusSecondEntry) { - this.ui.list.focus(QuickInputListFocus.Second); - this._autoFocusSecondEntry = false; // only valid once, then unset - } else { - this.trySelectFirst(); + switch (this._itemActivation) { + case ItemActivation.SECOND: + this.ui.list.focus(QuickInputListFocus.Second); + this._itemActivation = ItemActivation.FIRST; // only valid once, then unset + break; + case ItemActivation.LAST: + this.ui.list.focus(QuickInputListFocus.Last); + this._itemActivation = ItemActivation.FIRST; // only valid once, then unset + break; + default: + this.trySelectFirst(); + break; } } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index fb0e4f1fdd..61760489d9 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -496,7 +496,7 @@ export class QuickInputList { } set enabled(value: boolean) { - this.list.getHTMLElement().style.pointerEvents = value ? null : 'none'; + this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none'; } focus(what: QuickInputListFocus): void { diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 9e9934922e..3ff9e35ebb 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -182,6 +182,12 @@ export interface IQuickPickAcceptEvent { inBackground: boolean; } +export enum ItemActivation { + FIRST = 1, + SECOND, + LAST +} + export interface IQuickPick extends IQuickInput { value: string; @@ -237,20 +243,17 @@ export interface IQuickPick extends IQuickInput { autoFocusOnList: boolean; - /** - * If enabled, will try to select the second entry of the picks - * once they appear instead of the first one. This is useful - * e.g. when `quickNavigate` is enabled to be able to select - * a previous entry by just releasing the quick nav keys. - */ - autoFocusSecondEntry: boolean; - quickNavigate: IQuickNavigateConfiguration | undefined; activeItems: ReadonlyArray; readonly onDidChangeActive: Event; + /** + * Allows to control which entry should be activated by default. + */ + itemActivation: ItemActivation; + selectedItems: ReadonlyArray; readonly onDidChangeSelection: Event; diff --git a/src/vs/base/test/common/history.test.ts b/src/vs/base/test/common/history.test.ts index 20c19068ab..327478fb86 100644 --- a/src/vs/base/test/common/history.test.ts +++ b/src/vs/base/test/common/history.test.ts @@ -106,6 +106,40 @@ suite('History Navigator', () => { assert.deepEqual(['2', '3', '1'], toArray(testObject)); }); + test('previous returns null if the current position is the first one', () => { + const testObject = new HistoryNavigator(['1', '2', '3']); + + testObject.first(); + + assert.deepEqual(testObject.previous(), null); + }); + + test('previous returns object if the current position is not the first one', () => { + const testObject = new HistoryNavigator(['1', '2', '3']); + + testObject.first(); + testObject.next(); + + assert.deepEqual(testObject.previous(), '1'); + }); + + test('next returns null if the current position is the last one', () => { + const testObject = new HistoryNavigator(['1', '2', '3']); + + testObject.last(); + + assert.deepEqual(testObject.next(), null); + }); + + test('next returns object if the current position is not the last one', () => { + const testObject = new HistoryNavigator(['1', '2', '3']); + + testObject.last(); + testObject.previous(); + + assert.deepEqual(testObject.next(), '3'); + }); + test('clear', () => { const testObject = new HistoryNavigator(['a', 'b', 'c']); assert.equal(testObject.previous(), 'c'); diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 253ab63bd8..94bb23b1a6 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -503,4 +503,65 @@ suite('URI', () => { // } // console.profileEnd(); }); + function assertJoined(base: string, fragment: string, expected: string, checkWithUrl: boolean = true) { + const baseUri = URI.parse(base); + const newUri = URI.joinPath(baseUri, fragment); + const actual = newUri.toString(true); + assert.equal(actual, expected); + + if (checkWithUrl) { + const actualUrl = new URL(fragment, base).href; + assert.equal(actualUrl, expected, 'DIFFERENT from URL'); + } + } + test('URI#joinPath', function () { + + assertJoined(('file:///foo/'), '../../bazz', 'file:///bazz'); + assertJoined(('file:///foo'), '../../bazz', 'file:///bazz'); + assertJoined(('file:///foo'), '../../bazz', 'file:///bazz'); + assertJoined(('file:///foo/bar/'), './bazz', 'file:///foo/bar/bazz'); + assertJoined(('file:///foo/bar'), './bazz', 'file:///foo/bar/bazz', false); + assertJoined(('file:///foo/bar'), 'bazz', 'file:///foo/bar/bazz', false); + + // "auto-path" scheme + assertJoined(('file:'), 'bazz', 'file:///bazz'); + assertJoined(('http://domain'), 'bazz', 'http://domain/bazz'); + assertJoined(('https://domain'), 'bazz', 'https://domain/bazz'); + assertJoined(('http:'), 'bazz', 'http:/bazz', false); + assertJoined(('https:'), 'bazz', 'https:/bazz', false); + + // no "auto-path" scheme with and w/o paths + assertJoined(('foo:/'), 'bazz', 'foo:/bazz'); + assertJoined(('foo://bar/'), 'bazz', 'foo://bar/bazz'); + + // no "auto-path" + no path -> error + assert.throws(() => assertJoined(('foo:'), 'bazz', '')); + assert.throws(() => new URL('bazz', 'foo:')); + assert.throws(() => assertJoined(('foo://bar'), 'bazz', '')); + assert.throws(() => new URL('bazz', 'foo://bar')); + }); + + test('URI#joinPath (posix)', function () { + if (isWindows) { + this.skip(); + } + assertJoined(('file:///c:/foo/'), '../../bazz', 'file:///bazz', false); + assertJoined(('file://server/share/c:/'), '../../bazz', 'file://server/bazz', false); + assertJoined(('file://server/share/c:'), '../../bazz', 'file://server/bazz', false); + + assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/bazz'); + assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/bazz'); + }); + + test('URI#joinPath (windows)', function () { + if (!isWindows) { + this.skip(); + } + assertJoined(('file:///c:/foo/'), '../../bazz', 'file:///c:/bazz', false); + assertJoined(('file://server/share/c:/'), '../../bazz', 'file://server/share/bazz', false); + assertJoined(('file://server/share/c:'), '../../bazz', 'file://server/share/bazz', false); + + assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/foo/bazz', false); + assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/foo/bazz', false); + }); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 4f17847f81..30f927a65f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -52,7 +52,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -67,6 +67,7 @@ import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/u import { IAuthenticationTokenService, AuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { AuthenticationTokenServiceChannel } from 'vs/platform/authentication/common/authenticationIpc'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -140,6 +141,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IStorageService, storageService); disposables.add(toDisposable(() => storageService.flush())); + services.set(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryChannelClient(mainProcessService.getChannel('storageKeysSyncRegistryService'))); + services.set(IEnvironmentService, environmentService); services.set(IProductService, { _serviceBrand: undefined, ...product }); services.set(ILogService, logService); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 69e58428d6..4cf1232f3c 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -77,6 +77,8 @@ import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/elect import { withNullAsUndefined } from 'vs/base/common/types'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { coalesce } from 'vs/base/common/arrays'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -567,6 +569,11 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); + const storageKeysSyncRegistryService = accessor.get(IStorageKeysSyncRegistryService); + const storageKeysSyncChannel = new StorageKeysSyncRegistryChannel(storageKeysSyncRegistryService); + electronIpcServer.registerChannel('storageKeysSyncRegistryService', storageKeysSyncChannel); + sharedProcessClient.then(client => client.registerChannel('storageKeysSyncRegistryService', storageKeysSyncChannel)); + const loggerChannel = new LoggerChannel(accessor.get(ILogService)); electronIpcServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 541371e832..0aa58b3a18 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -46,6 +46,7 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; +import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; class ExpectedError extends Error { readonly isExpected = true; @@ -162,6 +163,7 @@ class CodeMain { services.set(IRequestService, new SyncDescriptor(RequestMainService)); services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); services.set(ISignService, new SyncDescriptor(SignService)); + services.set(IStorageKeysSyncRegistryService, new SyncDescriptor(StorageKeysSyncRegistryService)); return [new InstantiationService(services, true), instanceEnvironment]; } diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index eb9f1de262..9686283e1a 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -42,8 +42,8 @@ class MsPointerHandler extends MouseHandler implements IDisposable { constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) { super(context, viewController, viewHelper); - this.viewHelper.linesContentDomNode.style.msTouchAction = 'none'; - this.viewHelper.linesContentDomNode.style.msContentZooming = 'none'; + (this.viewHelper.linesContentDomNode.style as any).msTouchAction = 'none'; + (this.viewHelper.linesContentDomNode.style as any).msContentZooming = 'none'; // TODO@Alex -> this expects that the view is added in 100 ms, might not be the case // This handler should be added when the dom node is in the dom tree diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 3bb7a9ef74..61f3782e69 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -349,7 +349,7 @@ export class TextAreaHandler extends ViewPart { private _getAriaLabel(options: IComputedEditorOptions): string { const accessibilitySupport = options.get(EditorOption.accessibilitySupport); if (accessibilitySupport === AccessibilitySupport.Disabled) { - return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press Alt+F1 for options."); + return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press {0} for options.", platform.isLinux ? 'Shift+Alt+F1' : 'Alt+F1'); } return options.get(EditorOption.ariaLabel); } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 53afdd7410..e6c1b93384 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -23,6 +23,8 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { protected _contentLeft: number; protected _contentWidth: number; protected _selectionIsEmpty: boolean; + protected _renderLineHightlightOnlyWhenFocus: boolean; + protected _focused: boolean; private _cursorLineNumbers: number[]; private _selections: Selection[]; private _renderData: string[] | null; @@ -35,9 +37,11 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const layoutInfo = options.get(EditorOption.layoutInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); + this._renderLineHightlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._contentLeft = layoutInfo.contentLeft; this._contentWidth = layoutInfo.contentWidth; this._selectionIsEmpty = true; + this._focused = false; this._cursorLineNumbers = []; this._selections = []; this._renderData = null; @@ -81,6 +85,7 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const layoutInfo = options.get(EditorOption.layoutInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); + this._renderLineHightlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._contentLeft = layoutInfo.contentLeft; this._contentWidth = layoutInfo.contentWidth; return true; @@ -104,6 +109,14 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; } + public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { + if (!this._renderLineHightlightOnlyWhenFocus) { + return false; + } + + this._focused = e.isFocused; + return true; + } // --- end event handlers public prepareRender(ctx: RenderingContext): void { @@ -157,11 +170,13 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { return ( (this._renderLineHighlight === 'line' || this._renderLineHighlight === 'all') && this._selectionIsEmpty + && (!this._renderLineHightlightOnlyWhenFocus || this._focused) ); } protected _shouldRenderOther(): boolean { return ( (this._renderLineHighlight === 'gutter' || this._renderLineHighlight === 'all') + && (!this._renderLineHightlightOnlyWhenFocus || this._focused) ); } } @@ -174,12 +189,14 @@ export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOver protected _shouldRenderThis(): boolean { return ( (this._renderLineHighlight === 'gutter' || this._renderLineHighlight === 'all') + && (!this._renderLineHightlightOnlyWhenFocus || this._focused) ); } protected _shouldRenderOther(): boolean { return ( (this._renderLineHighlight === 'line' || this._renderLineHighlight === 'all') && this._selectionIsEmpty + && (!this._renderLineHightlightOnlyWhenFocus || this._focused) ); } } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 753c0de52a..5b7413ed77 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -546,6 +546,11 @@ export interface IEditorOptions { * Defaults to all. */ renderLineHighlight?: 'none' | 'gutter' | 'line' | 'all'; + /** + * Control if the current line highlight should be rendered only the editor is focused. + * Defaults to false. + */ + renderLineHighlightOnlyWhenFocus?: boolean; /** * Inserting and deleting whitespace follows tab stops. */ @@ -3414,6 +3419,7 @@ export const enum EditorOption { renderIndentGuides, renderFinalNewline, renderLineHighlight, + renderLineHighlightOnlyWhenFocus, renderValidationDecorations, renderWhitespace, revealHorizontalRightPadding, @@ -3856,6 +3862,10 @@ export const EditorOptions = { description: nls.localize('renderLineHighlight', "Controls how the editor should render the current line highlight.") } )), + renderLineHighlightOnlyWhenFocus: register(new EditorBooleanOption( + EditorOption.renderLineHighlightOnlyWhenFocus, 'renderLineHighlightOnlyWhenFocus', false, + { description: nls.localize('renderLineHighlightOnlyWhenFocus', "Controls if the editor should render the current line highlight only when the editor is focused") } + )), renderValidationDecorations: register(new EditorStringEnumOption( EditorOption.renderValidationDecorations, 'renderValidationDecorations', 'editable' as 'editable' | 'on' | 'off', diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 4fd9fb9842..c482ea526d 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -245,43 +245,44 @@ export enum EditorOption { renderIndentGuides = 75, renderFinalNewline = 76, renderLineHighlight = 77, - renderValidationDecorations = 78, - renderWhitespace = 79, - revealHorizontalRightPadding = 80, - roundedSelection = 81, - rulers = 82, - scrollbar = 83, - scrollBeyondLastColumn = 84, - scrollBeyondLastLine = 85, - scrollPredominantAxis = 86, - selectionClipboard = 87, - selectionHighlight = 88, - selectOnLineNumbers = 89, - showFoldingControls = 90, - showUnused = 91, - snippetSuggestions = 92, - smoothScrolling = 93, - stopRenderingLineAfter = 94, - suggest = 95, - suggestFontSize = 96, - suggestLineHeight = 97, - suggestOnTriggerCharacters = 98, - suggestSelection = 99, - tabCompletion = 100, - useTabStops = 101, - wordSeparators = 102, - wordWrap = 103, - wordWrapBreakAfterCharacters = 104, - wordWrapBreakBeforeCharacters = 105, - wordWrapColumn = 106, - wordWrapMinified = 107, - wrappingIndent = 108, - wrappingStrategy = 109, - editorClassName = 110, - pixelRatio = 111, - tabFocusMode = 112, - layoutInfo = 113, - wrappingInfo = 114 + renderLineHighlightOnlyWhenFocus = 78, + renderValidationDecorations = 79, + renderWhitespace = 80, + revealHorizontalRightPadding = 81, + roundedSelection = 82, + rulers = 83, + scrollbar = 84, + scrollBeyondLastColumn = 85, + scrollBeyondLastLine = 86, + scrollPredominantAxis = 87, + selectionClipboard = 88, + selectionHighlight = 89, + selectOnLineNumbers = 90, + showFoldingControls = 91, + showUnused = 92, + snippetSuggestions = 93, + smoothScrolling = 94, + stopRenderingLineAfter = 95, + suggest = 96, + suggestFontSize = 97, + suggestLineHeight = 98, + suggestOnTriggerCharacters = 99, + suggestSelection = 100, + tabCompletion = 101, + useTabStops = 102, + wordSeparators = 103, + wordWrap = 104, + wordWrapBreakAfterCharacters = 105, + wordWrapBreakBeforeCharacters = 106, + wordWrapColumn = 107, + wordWrapMinified = 108, + wrappingIndent = 109, + wrappingStrategy = 110, + editorClassName = 111, + pixelRatio = 112, + tabFocusMode = 113, + layoutInfo = 114, + wrappingInfo = 115 } /** diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index 1697e1ad81..c5b306ff8e 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -11,7 +11,7 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -47,13 +47,12 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; // Provide based on current active editor - let pickerDisposable = this.doProvide(picker, token); - disposables.add(toDisposable(() => pickerDisposable.dispose())); + const pickerDisposable = disposables.add(new MutableDisposable()); + pickerDisposable.value = this.doProvide(picker, token); // Re-create whenever the active editor changes disposables.add(this.onDidActiveTextEditorControlChange(() => { - pickerDisposable.dispose(); - pickerDisposable = this.doProvide(picker, token); + pickerDisposable.value = this.doProvide(picker, token); })); return disposables; @@ -81,7 +80,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu })); disposables.add(once(token.onCancellationRequested)(() => { - if (lastKnownEditorViewState) { + if (lastKnownEditorViewState && editor === this.activeTextEditorControl) { editor.restoreViewState(lastKnownEditorViewState); } })); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 16f6718550..3b492f48ec 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -179,11 +179,11 @@ declare namespace monaco { /** * Join a Uri path with path fragments and normalizes the resulting path. * - * @param resource The input Uri. + * @param uri The input Uri. * @param pathFragment The path fragment to add to the Uri path. * @returns The resulting Uri. */ - static joinPaths(resource: Uri, ...pathFragment: string[]): Uri; + static joinPath(uri: Uri, ...pathFragment: string[]): Uri; /** * Creates a string representation for this Uri. It's guaranteed that calling * `Uri.parse` with the result of this function creates an Uri which is equal @@ -3081,6 +3081,11 @@ declare namespace monaco.editor { * Defaults to all. */ renderLineHighlight?: 'none' | 'gutter' | 'line' | 'all'; + /** + * Control if the current line highlight should be rendered only the editor is focused. + * Defaults to false. + */ + renderLineHighlightOnlyWhenFocus?: boolean; /** * Inserting and deleting whitespace follows tab stops. */ @@ -3901,43 +3906,44 @@ declare namespace monaco.editor { renderIndentGuides = 75, renderFinalNewline = 76, renderLineHighlight = 77, - renderValidationDecorations = 78, - renderWhitespace = 79, - revealHorizontalRightPadding = 80, - roundedSelection = 81, - rulers = 82, - scrollbar = 83, - scrollBeyondLastColumn = 84, - scrollBeyondLastLine = 85, - scrollPredominantAxis = 86, - selectionClipboard = 87, - selectionHighlight = 88, - selectOnLineNumbers = 89, - showFoldingControls = 90, - showUnused = 91, - snippetSuggestions = 92, - smoothScrolling = 93, - stopRenderingLineAfter = 94, - suggest = 95, - suggestFontSize = 96, - suggestLineHeight = 97, - suggestOnTriggerCharacters = 98, - suggestSelection = 99, - tabCompletion = 100, - useTabStops = 101, - wordSeparators = 102, - wordWrap = 103, - wordWrapBreakAfterCharacters = 104, - wordWrapBreakBeforeCharacters = 105, - wordWrapColumn = 106, - wordWrapMinified = 107, - wrappingIndent = 108, - wrappingStrategy = 109, - editorClassName = 110, - pixelRatio = 111, - tabFocusMode = 112, - layoutInfo = 113, - wrappingInfo = 114 + renderLineHighlightOnlyWhenFocus = 78, + renderValidationDecorations = 79, + renderWhitespace = 80, + revealHorizontalRightPadding = 81, + roundedSelection = 82, + rulers = 83, + scrollbar = 84, + scrollBeyondLastColumn = 85, + scrollBeyondLastLine = 86, + scrollPredominantAxis = 87, + selectionClipboard = 88, + selectionHighlight = 89, + selectOnLineNumbers = 90, + showFoldingControls = 91, + showUnused = 92, + snippetSuggestions = 93, + smoothScrolling = 94, + stopRenderingLineAfter = 95, + suggest = 96, + suggestFontSize = 97, + suggestLineHeight = 98, + suggestOnTriggerCharacters = 99, + suggestSelection = 100, + tabCompletion = 101, + useTabStops = 102, + wordSeparators = 103, + wordWrap = 104, + wordWrapBreakAfterCharacters = 105, + wordWrapBreakBeforeCharacters = 106, + wordWrapColumn = 107, + wordWrapMinified = 108, + wrappingIndent = 109, + wrappingStrategy = 110, + editorClassName = 111, + pixelRatio = 112, + tabFocusMode = 113, + layoutInfo = 114, + wrappingInfo = 115 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4018,6 +4024,7 @@ declare namespace monaco.editor { renderIndentGuides: IEditorOption; renderFinalNewline: IEditorOption; renderLineHighlight: IEditorOption; + renderLineHighlightOnlyWhenFocus: IEditorOption; renderValidationDecorations: IEditorOption; renderWhitespace: IEditorOption; revealHorizontalRightPadding: IEditorOption; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 9b94c102ea..bd12b01090 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -43,16 +43,14 @@ export const OPTIONS: OptionDescriptions> = { 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, + 'folder-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('folderUri', "Opens a window with given folder uri(s)") }, + 'file-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('fileUri', "Opens a window with given file uri(s)") }, 'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") }, 'waitMarkerFilePath': { type: 'string' }, 'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") }, 'user-data-dir': { type: 'string', cat: 'o', args: 'dir', description: localize('userDataDir', "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.") }, - 'version': { type: 'boolean', cat: 'o', alias: 'v', description: localize('version', "Print version.") }, - 'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") }, 'telemetry': { type: 'boolean', cat: 'o', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, - 'folder-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('folderUri', "Opens a window with given folder uri(s)") }, - 'file-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('fileUri', "Opens a window with given file uri(s)") }, - 'sync': { type: 'string', cat: 'o', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, + 'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") }, 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'builtin-extensions-dir': { type: 'string' }, @@ -63,6 +61,7 @@ export const OPTIONS: OptionDescriptions> = { 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, + 'version': { type: 'boolean', cat: 't', alias: 'v', description: localize('version', "Print version.") }, 'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") }, 'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") }, 'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") }, @@ -71,6 +70,7 @@ export const OPTIONS: OptionDescriptions> = { 'prof-startup-prefix': { type: 'string' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, + 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, 'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bd61283587..62af51bc63 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -86,7 +86,7 @@ function readManifest(extensionPath: string): Promise<{ manifest: IExtensionMani .then(raw => JSON.parse(raw)) ]; - return Promise.all(promises).then(([{ manifest, metadata }, translations]) => { + return Promise.all(promises).then(([{ manifest, metadata }, translations]) => { return { manifest: localizeManifest(manifest, translations), metadata @@ -348,7 +348,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this.downloadInstallableExtension(extension, operation) .then(installableExtension => this.installExtension(installableExtension, ExtensionType.User, cancellationToken) - .then(local => pfs.rimraf(installableExtension.zipPath).finally(() => null).then(() => local))) + .then(local => pfs.rimraf(installableExtension.zipPath).finally(() => { }).then(() => local))) .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) .then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error)))) .then( @@ -503,7 +503,7 @@ export class ExtensionManagementService extends Disposable implements IExtension () => this.logService.info('Renamed to', renamePath), e => { this.logService.info('Rename failed. Deleting from extracted location', extractPath); - return pfs.rimraf(extractPath).finally(() => null).then(() => Promise.reject(e)); + return pfs.rimraf(extractPath).finally(() => { }).then(() => Promise.reject(e)); })); } @@ -514,7 +514,7 @@ export class ExtensionManagementService extends Disposable implements IExtension () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, token) .then( () => this.logService.info(`Extracted extension to ${extractPath}:`, identifier.id), - e => pfs.rimraf(extractPath).finally(() => null) + e => pfs.rimraf(extractPath).finally(() => { }) .then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))), e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); } diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index cadf6434f9..684487331f 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -55,12 +55,17 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc // Ask subclass for all command picks const allCommandPicks = await this.getCommandPicks(disposables, token); + if (token.isCancellationRequested) { + return []; + } + // Filter const filteredCommandPicks: ICommandQuickPick[] = []; for (const commandPick of allCommandPicks) { const labelHighlights = withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.label)); const aliasHighlights = commandPick.commandAlias ? withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.commandAlias)) : undefined; + // Add if matching in label or alias if (labelHighlights || aliasHighlights) { commandPick.highlights = { label: labelHighlights, @@ -69,6 +74,11 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc filteredCommandPicks.push(commandPick); } + + // Also add if we have a 100% command ID match + else if (filter === commandPick.commandId) { + filteredCommandPicks.push(commandPick); + } } // Add description to commands that have duplicate labels @@ -130,7 +140,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc commandPicks.push({ ...commandPick, ariaLabel, - detail: this.options.showAlias ? commandPick.commandAlias : undefined, + detail: this.options.showAlias && commandPick.commandAlias !== commandPick.label ? commandPick.commandAlias : undefined, keybinding, accept: async () => { diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 55ec610520..5b0500ce10 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -7,7 +7,7 @@ import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickI import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; -import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; export enum TriggerAction { @@ -90,7 +90,9 @@ export abstract class PickerQuickAccessProvider { + const picksDisposables = picksDisposable.value = new DisposableStore(); // Cancel any previous ask for picks and busy picksCts?.dispose(true); @@ -101,7 +103,7 @@ export abstract class PickerQuickAccessProvider value.substring(descriptor ? descriptor.prefix.length : 0); // Register listeners - const cancellationToken = this.registerPickerListeners(disposables, picker, provider, descriptor, value); + const cancellationToken = this.registerPickerListeners(picker, provider, descriptor, value, disposables); // Ask provider to fill the picker as needed if we have one if (provider) { @@ -136,7 +132,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon picker.valueSelection = valueSelection; } - private registerPickerListeners(disposables: DisposableStore, picker: IQuickPick, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string): CancellationToken { + private registerPickerListeners(picker: IQuickPick, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, disposables: DisposableStore): CancellationToken { // Remember as last visible picker and clean up once picker get's disposed const visibleQuickAccess = this.visibleQuickAccess = { picker, descriptor, value }; diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 365471bf79..dd84531e06 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -9,6 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { first, coalesce } from 'vs/base/common/arrays'; import { startsWith } from 'vs/base/common/strings'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ItemActivation } from 'vs/base/parts/quickinput/common/quickInput'; export interface IQuickAccessOptions { @@ -18,9 +19,10 @@ export interface IQuickAccessOptions { quickNavigateConfiguration?: IQuickNavigateConfiguration; /** - * Wether to select the second pick item by default instead of the first. + * Allows to configure a different item activation strategy. + * By default the first item in the list will get activated. */ - autoFocus?: { autoFocusSecondEntry?: boolean } + itemActivation?: ItemActivation } export interface IQuickAccessController { diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts index b7f7f4ba79..d32ec46853 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -4,63 +4,120 @@ *--------------------------------------------------------------------------------------------*/ import * as objects from 'vs/base/common/objects'; -import { IGlobalState } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync'; import { IStringDictionary } from 'vs/base/common/collections'; import { values } from 'vs/base/common/map'; +import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; +import { ILogService } from 'vs/platform/log/common/log'; -export function merge(localGloablState: IGlobalState, remoteGlobalState: IGlobalState | null, lastSyncGlobalState: IGlobalState | null): { local?: IGlobalState, remote?: IGlobalState } { - if (!remoteGlobalState) { - return { remote: localGloablState }; - } - - const { local: localArgv, remote: remoteArgv } = doMerge(localGloablState.argv, remoteGlobalState.argv, lastSyncGlobalState ? lastSyncGlobalState.argv : null); - const { local: localStorage, remote: remoteStorage } = doMerge(localGloablState.storage, remoteGlobalState.storage, lastSyncGlobalState ? lastSyncGlobalState.storage : null); - const local: IGlobalState | undefined = localArgv || localStorage ? { argv: localArgv || localGloablState.argv, storage: localStorage || localGloablState.storage } : undefined; - const remote: IGlobalState | undefined = remoteArgv || remoteStorage ? { argv: remoteArgv || remoteGlobalState.argv, storage: remoteStorage || remoteGlobalState.storage } : undefined; - - return { local, remote }; +export interface IMergeResult { + local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; + remote: IStringDictionary | null; } -function doMerge(local: IStringDictionary, remote: IStringDictionary, base: IStringDictionary | null): { local?: IStringDictionary, remote?: IStringDictionary } { - const localToRemote = compare(local, remote); +export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, logService: ILogService): IMergeResult { + if (!remoteStorage) { + return { remote: localStorage, local: { added: {}, removed: [], updated: {} } }; + } + + const localToRemote = compare(localStorage, remoteStorage); if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. - return {}; + return { remote: null, local: { added: {}, removed: [], updated: {} } }; } - const baseToRemote = base ? compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - if (baseToRemote.added.size === 0 && baseToRemote.removed.size === 0 && baseToRemote.updated.size === 0) { - // No changes found between base and remote. - return { remote: local }; - } + const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + const baseToLocal = baseStorage ? compare(baseStorage, localStorage) : { added: Object.keys(localStorage).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - const baseToLocal = base ? compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - if (baseToLocal.added.size === 0 && baseToLocal.removed.size === 0 && baseToLocal.updated.size === 0) { - // No changes found between base and local. - return { local: remote }; - } - - const merged = objects.deepClone(local); + const local: { added: IStringDictionary, removed: string[], updated: IStringDictionary } = { added: {}, removed: [], updated: {} }; + const remote: IStringDictionary = objects.deepClone(remoteStorage); // Added in remote for (const key of values(baseToRemote.added)) { - merged[key] = remote[key]; + const remoteValue = remoteStorage[key]; + const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; + if (!storageKey) { + logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + continue; + } + if (storageKey.version !== remoteValue.version) { + logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); + continue; + } + const localValue = localStorage[key]; + if (localValue && localValue.value === remoteValue.value) { + continue; + } + if (baseToLocal.added.has(key)) { + local.updated[key] = remoteValue; + } else { + local.added[key] = remoteValue; + } } // Updated in Remote for (const key of values(baseToRemote.updated)) { - merged[key] = remote[key]; + const remoteValue = remoteStorage[key]; + const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; + if (!storageKey) { + logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + continue; + } + if (storageKey.version !== remoteValue.version) { + logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); + continue; + } + const localValue = localStorage[key]; + if (localValue && localValue.value === remoteValue.value) { + continue; + } + local.updated[key] = remoteValue; } - // Removed in remote & local + // Removed in remote for (const key of values(baseToRemote.removed)) { - // Got removed in local - if (baseToLocal.removed.has(key)) { - delete merged[key]; + const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; + if (!storageKey) { + logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + continue; + } + local.removed.push(key); + } + + // Added in local + for (const key of values(baseToLocal.added)) { + if (!baseToRemote.added.has(key)) { + remote[key] = localStorage[key]; } } - return { local: merged, remote: merged }; + // Updated in local + for (const key of values(baseToLocal.updated)) { + if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) { + continue; + } + const remoteValue = remote[key]; + const localValue = localStorage[key]; + if (localValue.version < remoteValue.version) { + continue; + } + remote[key] = localValue; + } + + // Removed in local + for (const key of values(baseToLocal.removed)) { + if (baseToRemote.updated.has(key)) { + continue; + } + const remoteValue = remote[key]; + const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; + if (storageKey && storageKey.version < remoteValue.version) { + continue; + } + delete remote[key]; + } + + return { local, remote: areSame(remote, remoteStorage) ? null : remote }; } function compare(from: IStringDictionary, to: IStringDictionary): { added: Set, removed: Set, updated: Set } { @@ -83,3 +140,9 @@ function compare(from: IStringDictionary, to: IStringDictionary): { ad return { added, removed, updated }; } + +function areSame(a: IStringDictionary, b: IStringDictionary): boolean { + const { added, removed, updated } = compare(a, b); + return added.size === 0 && removed.size === 0 && updated.size === 0; +} + diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 4588b00618..2043bb3178 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -19,12 +19,15 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { URI } from 'vs/base/common/uri'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; +const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; interface ISyncPreviewResult { - readonly local: IGlobalState | undefined; - readonly remote: IGlobalState | undefined; + readonly local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; + readonly remote: IStringDictionary | null; readonly localUserData: IGlobalState; readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; @@ -43,10 +46,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService, + @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); + this._register(Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key))(() => this._onDidChangeLocal.fire())); } async pull(): Promise { @@ -65,9 +71,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const remoteUserData = await this.getRemoteUserData(lastSyncUserData); if (remoteUserData.syncData !== null) { - const localUserData = await this.getLocalGlobalState(); - const local: IGlobalState = JSON.parse(remoteUserData.syncData.content); - await this.apply({ local, remote: undefined, remoteUserData, localUserData, lastSyncUserData }); + const localGlobalState = await this.getLocalGlobalState(); + const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content); + const { local, remote } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), this.logService); + await this.apply({ local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData }); } // No remote exists to pull @@ -96,7 +103,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const localUserData = await this.getLocalGlobalState(); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - await this.apply({ local: undefined, remote: localUserData, remoteUserData, localUserData, lastSyncUserData }, true); + await this.apply({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData }, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`); } finally { @@ -136,8 +143,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async hasLocalData(): Promise { try { - const localGloablState = await this.getLocalGlobalState(); - if (localGloablState.argv['locale'] !== 'en') { + const { storage } = await this.getLocalGlobalState(); + if (Object.keys(storage).length > 1 || storage[`${argvStoragePrefx}.locale`]?.value !== 'en') { return true; } } catch (error) { @@ -154,7 +161,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; - const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; + const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; const localGloablState = await this.getLocalGlobalState(); @@ -164,20 +171,21 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`); } - const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState); + const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), this.logService); return { local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData }; } private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData }: ISyncPreviewResult, forcePush?: boolean): Promise { - const hasChanges = local || remote; + const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0; + const hasRemoteChanged = remote !== null; - if (!hasChanges) { + if (!hasLocalChanged && !hasRemoteChanged) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); } - if (local) { + if (hasLocalChanged) { // update local this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`); await this.backupLocal(JSON.stringify(localUserData)); @@ -185,10 +193,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`); } - if (remote) { + if (hasRemoteChanged) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); - const content = JSON.stringify(remote); + const content = JSON.stringify({ storage: remote }); remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`); } @@ -202,16 +210,21 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } private async getLocalGlobalState(): Promise { - const argv: IStringDictionary = {}; - const storage: IStringDictionary = {}; + const storage: IStringDictionary = {}; const argvContent: string = await this.getLocalArgvContent(); const argvValue: IStringDictionary = parse(argvContent); for (const argvProperty of argvProperties) { if (argvValue[argvProperty] !== undefined) { - argv[argvProperty] = argvValue[argvProperty]; + storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] }; } } - return { argv, storage }; + for (const { key, version } of this.storageKeysSyncRegistryService.storageKeys) { + const value = this.storageService.get(key, StorageScope.GLOBAL); + if (value) { + storage[key] = { version, value }; + } + } + return { storage }; } private async getLocalArgvContent(): Promise { @@ -222,15 +235,59 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return '{}'; } - private async writeLocalGlobalState(globalState: IGlobalState): Promise { - const argvContent = await this.getLocalArgvContent(); - let content = argvContent; - for (const argvProperty of Object.keys(globalState.argv)) { - content = edit(content, [argvProperty], globalState.argv[argvProperty], {}); + private async writeLocalGlobalState({ added, removed, updated }: { added: IStringDictionary, updated: IStringDictionary, removed: string[] }): Promise { + const argv: IStringDictionary = {}; + const updatedStorage: IStringDictionary = {}; + const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary): void => { + for (const key of keys) { + if (key.startsWith(argvStoragePrefx)) { + argv[key.substring(argvStoragePrefx.length)] = storage ? storage[key].value : undefined; + continue; + } + if (storage) { + const storageValue = storage[key]; + if (storageValue.value !== String(this.storageService.get(key, StorageScope.GLOBAL))) { + updatedStorage[key] = storageValue.value; + } + } else { + if (this.storageService.get(key, StorageScope.GLOBAL) !== undefined) { + updatedStorage[key] = undefined; + } + } + } + }; + handleUpdatedStorage(Object.keys(added), added); + handleUpdatedStorage(Object.keys(updated), updated); + handleUpdatedStorage(removed); + if (Object.keys(argv).length) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`); + await this.updateArgv(argv); + this.logService.info(`${this.syncResourceLogLabel}: Updated locale`); } - if (argvContent !== content) { - await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); + const updatedStorageKeys: string[] = Object.keys(updatedStorage); + if (updatedStorageKeys.length) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`); + for (const key of Object.keys(updatedStorage)) { + this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL); + } + this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage)); } } + private async updateArgv(argv: IStringDictionary): Promise { + const argvContent = await this.getLocalArgvContent(); + let content = argvContent; + for (const argvProperty of Object.keys(argv)) { + content = edit(content, [argvProperty], argv[argvProperty], {}); + } + if (argvContent !== content) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`); + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); + this.logService.info(`${this.syncResourceLogLabel}: Updated locale.`); + } + } + + private getSyncStorageKeys(): IStorageKey[] { + return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => ({ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))]; + } } diff --git a/src/vs/platform/userDataSync/common/storageKeys.ts b/src/vs/platform/userDataSync/common/storageKeys.ts new file mode 100644 index 0000000000..ad0a82a54b --- /dev/null +++ b/src/vs/platform/userDataSync/common/storageKeys.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { values } from 'vs/base/common/map'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface IStorageKey { + + readonly key: string; + readonly version: number; + +} + +export const IStorageKeysSyncRegistryService = createDecorator('IStorageKeysSyncRegistryService'); + +export interface IStorageKeysSyncRegistryService { + + _serviceBrand: any; + + /** + * All registered storage keys + */ + readonly storageKeys: ReadonlyArray; + + /** + * Event that is triggered when storage keys are changed + */ + readonly onDidChangeStorageKeys: Event>; + + /** + * Register a storage key that has to be synchronized during sync. + */ + registerStorageKey(key: IStorageKey): void; + +} + +export class StorageKeysSyncRegistryService extends Disposable implements IStorageKeysSyncRegistryService { + + _serviceBrand: any; + + private readonly _storageKeys = new Map(); + get storageKeys(): ReadonlyArray { return values(this._storageKeys); } + + private readonly _onDidChangeStorageKeys: Emitter> = this._register(new Emitter>()); + readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event; + + constructor() { + super(); + this._register(toDisposable(() => this._storageKeys.clear())); + } + + registerStorageKey(storageKey: IStorageKey): void { + if (!this._storageKeys.has(storageKey.key)) { + this._storageKeys.set(storageKey.key, storageKey); + this._onDidChangeStorageKeys.fire(this.storageKeys); + } + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index d1ffd4643f..a6f43f528d 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -125,7 +125,7 @@ export interface IUserDataSyncStore { } export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = productService[CONFIGURATION_SYNC_STORE_KEY] || configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); + const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY) || productService[CONFIGURATION_SYNC_STORE_KEY]; if (value && value.url && value.authenticationProviderId) { return { url: joinPath(URI.parse(value.url), 'v1'), @@ -231,9 +231,13 @@ export interface ISyncExtension { disabled?: boolean; } +export interface IStorageValue { + version: number; + value: string; +} + export interface IGlobalState { - argv: IStringDictionary; - storage: IStringDictionary; + storage: IStringDictionary; } export const enum SyncStatus { diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 3c7fae47bf..809a73b39e 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; +import { Disposable } from 'vs/base/common/lifecycle'; export class UserDataSyncChannel implements IServerChannel { @@ -101,3 +103,51 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService { } } + +export class StorageKeysSyncRegistryChannel implements IServerChannel { + + constructor(private readonly service: IStorageKeysSyncRegistryService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeStorageKeys': return this.service.onDidChangeStorageKeys; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case '_getInitialData': return Promise.resolve(this.service.storageKeys); + case 'registerStorageKey': return Promise.resolve(this.service.registerStorageKey(args[0])); + } + throw new Error('Invalid call'); + } +} + +export class StorageKeysSyncRegistryChannelClient extends Disposable implements IStorageKeysSyncRegistryService { + + _serviceBrand: undefined; + + private _storageKeys: ReadonlyArray = []; + get storageKeys(): ReadonlyArray { return this._storageKeys; } + private readonly _onDidChangeStorageKeys: Emitter> = this._register(new Emitter>()); + readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event; + + constructor(private readonly channel: IChannel) { + super(); + this.channel.call('_getInitialData').then(storageKeys => { + this.updateStorageKeys(storageKeys); + this._register(this.channel.listen>('onDidChangeStorageKeys')(storageKeys => this.updateStorageKeys(storageKeys))); + }); + } + + private async updateStorageKeys(storageKeys: ReadonlyArray): Promise { + this._storageKeys = storageKeys; + this._onDidChangeStorageKeys.fire(this.storageKeys); + } + + registerStorageKey(storageKey: IStorageKey): void { + this.channel.call('registerStorageKey', [storageKey]); + } + +} diff --git a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts new file mode 100644 index 0000000000..549f985830 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; +import { NullLogService } from 'vs/platform/log/common/log'; + +suite('GlobalStateMerge', () => { + + test('merge when local and remote are same with one value', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries in different order', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote are same with different base content', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + const base = { 'b': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entry is added to remote', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when multiple new entries are added to remote', async () => { + const local = {}; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when new entry is added to remote from base and local has not changed', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when an entry is removed from remote from base and local has not changed', async () => { + const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, ['b']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when all entries are removed from base and local has not changed', async () => { + const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + const remote = {}; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, ['b', 'a']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when an entry is updated in remote from base and local has not changed', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } }; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } }); + assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } }); + assert.deepEqual(actual.local.removed, ['b']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when new entries are added to local', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when multiple new entries are added to local from base and remote is not changed', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } }; + const remote = { 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when an entry is removed from local from base and remote has not changed', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when an entry is updated in local from base and remote has not changed', async () => { + const local = { 'a': { version: 1, value: 'b' } }; + const remote = { 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => { + const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local and remote with one entry but different value', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => { + const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } }; + const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, ['b']); + assert.deepEqual(actual.remote, null); + }); + + test('merge with single entry and local is empty', async () => { + const base = { 'a': { version: 1, value: 'a' } }; + const local = {}; + const remote = { 'a': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote has moved forwareded with conflicts', async () => { + const base = { 'a': { version: 1, value: 'a' } }; + const local = { 'a': { version: 1, value: 'd' } }; + const remote = { 'a': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entry is added to remote but not a registered key', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entry is added to remote but different version', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when an entry is updated to remote but not a registered key', async () => { + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'a': { version: 1, value: 'b' } }; + + const actual = merge(local, remote, local, [], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entry is updated to remote but different version', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a local value is update with lower version', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } }; + const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a local value is update with higher version', async () => { + const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when a local value is removed but not registered', async () => { + const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a local value is removed with lower version', async () => { + const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a local value is removed with higher version', async () => { + const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, local); + }); + +}); diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts new file mode 100644 index 0000000000..bc41e15487 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, IGlobalState } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; + + +suite('GlobalStateSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let testClient: UserDataSyncClient; + let client2: UserDataSyncClient; + + let testObject: GlobalStateSynchroniser; + + setup(async () => { + testClient = disposableStore.add(new UserDataSyncClient(server)); + await testClient.setUp(true); + let storageKeysSyncRegistryService = testClient.instantiationService.get(IStorageKeysSyncRegistryService); + storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 }); + testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.GlobalState) as GlobalStateSynchroniser; + disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear())); + + client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + storageKeysSyncRegistryService = client2.instantiationService.get(IStorageKeysSyncRegistryService); + storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 }); + }); + + teardown(() => disposableStore.clear()); + + test('first time sync - outgoing to server (no state)', async () => { + updateStorage('a', 'value1', testClient); + await updateLocale(testClient); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'globalState.argv.locale': { version: 1, value: 'en' }, 'a': { version: 1, value: 'value1' } }); + }); + + test('first time sync - incoming from server (no state)', async () => { + updateStorage('a', 'value1', client2); + await updateLocale(client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + assert.equal(await readLocale(testClient), 'en'); + }); + + test('first time sync when storage exists', async () => { + updateStorage('a', 'value1', client2); + await client2.sync(); + + updateStorage('b', 'value2', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + assert.equal(readStorage('b', testClient), 'value2'); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } }); + }); + + test('first time sync when storage exists - has conflicts', async () => { + updateStorage('a', 'value1', client2); + await client2.sync(); + + updateStorage('a', 'value2', client2); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } }); + }); + + test('sync adding a storage value', async () => { + updateStorage('a', 'value1', testClient); + await testObject.sync(); + + updateStorage('b', 'value2', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + assert.equal(readStorage('b', testClient), 'value2'); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } }); + }); + + test('sync updating a storage value', async () => { + updateStorage('a', 'value1', testClient); + await testObject.sync(); + + updateStorage('a', 'value2', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value2'); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value2' } }); + }); + + test('sync removing a storage value', async () => { + updateStorage('a', 'value1', testClient); + updateStorage('b', 'value2', testClient); + await testObject.sync(); + + removeStorage('b', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + assert.equal(readStorage('b', testClient), undefined); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } }); + }); + + test('first time sync - push', async () => { + updateStorage('a', 'value1', testClient); + updateStorage('b', 'value2', testClient); + + await testObject.push(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseGlobalState(content!); + assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } }); + }); + + test('first time sync - pull', async () => { + updateStorage('a', 'value1', client2); + updateStorage('b', 'value2', client2); + await client2.sync(); + + await testObject.pull(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + assert.equal(readStorage('a', testClient), 'value1'); + assert.equal(readStorage('b', testClient), 'value2'); + }); + + function parseGlobalState(content: string): IGlobalState { + const syncData: ISyncData = JSON.parse(content); + return JSON.parse(syncData.content); + } + + async function updateLocale(client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' }))); + } + + function updateStorage(key: string, value: string, client: UserDataSyncClient): void { + const storageService = client.instantiationService.get(IStorageService); + storageService.store(key, value, StorageScope.GLOBAL); + } + + function removeStorage(key: string, client: UserDataSyncClient): void { + const storageService = client.instantiationService.get(IStorageService); + storageService.remove(key, StorageScope.GLOBAL); + } + + function readStorage(key: string, client: UserDataSyncClient): string | undefined { + const storageService = client.instantiationService.get(IStorageService); + return storageService.get(key, StorageScope.GLOBAL); + } + + async function readLocale(client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const content = await fileService.readFile(environmentService.argvResource); + return JSON.parse(content.value.toString()).locale; + } + +}); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index e62499294d..60cdc364e2 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -68,7 +68,7 @@ suite('TestSynchronizer', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -85,7 +85,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -97,7 +97,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); @@ -110,7 +110,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { error: true }; testObject.syncBarrier.open(); @@ -127,7 +127,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(); @@ -144,7 +144,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resource, false); const actual: SyncStatus[] = []; @@ -157,7 +157,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); await testObject.sync(); @@ -171,7 +171,7 @@ suite('TestSynchronizer', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); // Sync once testObject.syncBarrier.open(); await testObject.sync(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index a4d310364a..5a0e97c386 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -36,6 +36,7 @@ import { IAuthenticationTokenService } from 'vs/platform/authentication/common/a import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; +import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export class UserDataSyncClient extends Disposable { @@ -92,6 +93,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService)); + this.instantiationService.stub(IStorageKeysSyncRegistryService, this.instantiationService.createInstance(StorageKeysSyncRegistryService)); this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); this.instantiationService.stub(IExtensionManagementService, >{ diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 792028dfed..0230612342 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -234,7 +234,6 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, ]); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b38cc8494e..a717489d08 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2100,7 +2100,7 @@ declare module 'vscode' { * @param pathFragments * @returns A new uri */ - export function joinPaths(base: Uri, ...pathFragments: string[]): Uri; + export function joinPath(base: Uri, ...pathFragments: string[]): Uri; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 204830ae74..39eeb6ae5d 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -452,6 +452,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments id: COMMENTS_VIEW_ID, name: COMMENTS_VIEW_TITLE, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + hideIfEmpty: true, order: 10, }, ViewContainerLocation.Panel); @@ -460,6 +461,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments name: COMMENTS_VIEW_TITLE, canToggleVisibility: false, ctorDescriptor: new SyncDescriptor(CommentsPanel), + canMoveView: true, focusCommand: { id: 'workbench.action.focusCommentsPanel' } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 8ebd1e8e42..b7429187af 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -199,6 +199,14 @@ export class MainThreadNotebookController implements IMainNotebookController { let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); if (notebookHandle !== undefined) { mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + if (mainthreadNotebook && mainthreadNotebook.textModel.cells.length === 0) { + // it's empty, we should create an empty template one + const templateCell = await this._proxy.$createEmptyCell(this._viewType, uri, 0, mainthreadNotebook.textModel.languages.length ? mainthreadNotebook.textModel.languages[0] : '', CellKind.Code); + if (templateCell) { + let mainCell = new NotebookCellTextModel(URI.revive(templateCell.uri), templateCell.handle, templateCell.source, templateCell.language, templateCell.cellKind, templateCell.outputs, templateCell.metadata); + mainthreadNotebook.textModel.insertTemplateCell(mainCell); + } + } return mainthreadNotebook?.textModel; } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 57a90a5974..5108b006c2 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -64,6 +64,11 @@ export const viewsContainersContribution: IJSONSchema = { description: localize('views.container.activitybar', "Contribute views containers to Activity Bar"), type: 'array', items: viewsContainerSchema + }, + 'panel': { + description: localize('views.container.panel', "Contribute views containers to Panel"), + type: 'array', + items: viewsContainerSchema } } }; @@ -214,7 +219,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - let order = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId).length + 1; + let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1; + let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1; for (let { value, collector, description } of extensionPoints) { forEach(value, entry => { if (!this.isValidViewsContainer(entry.value, collector)) { @@ -222,7 +228,10 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } switch (entry.key) { case 'activitybar': - order = this.registerCustomViewContainers(entry.value, description, order, existingViewContainers); + activityBarOrder = this.registerCustomViewContainers(entry.value, description, activityBarOrder, existingViewContainers, ViewContainerLocation.Sidebar); + break; + case 'panel': + panelOrder = this.registerCustomViewContainers(entry.value, description, panelOrder, existingViewContainers, ViewContainerLocation.Panel); break; } }); @@ -248,7 +257,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const title = localize('test', "Test"); const icon = URI.parse(require.toUrl('./media/test.svg')); - this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined); + this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar); } private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { @@ -279,11 +288,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return true; } - private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[]): number { + private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[], location: ViewContainerLocation): number { containers.forEach(descriptor => { const icon = resources.joinPath(extension.extensionLocation, descriptor.icon); const id = `workbench.view.extension.${descriptor.id}`; - const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier); + const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier, location); // Move those views that belongs to this container if (existingViewContainers.length) { @@ -301,7 +310,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return order; } - private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, extensionId: ExtensionIdentifier | undefined): ViewContainer { + private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, extensionId: ExtensionIdentifier | undefined, location: ViewContainerLocation): ViewContainer { let viewContainer = this.viewContainersRegistry.get(id); if (!viewContainer) { @@ -316,7 +325,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { hideIfEmpty: true, order, icon, - }, ViewContainerLocation.Sidebar); + }, location); // Register Action to Open Viewlet class OpenCustomViewletAction extends ShowViewletAction { diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 48e57c87e2..64e368670b 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -24,7 +24,7 @@ import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleConte import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -598,6 +598,49 @@ export class MoveFocusedViewAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory, FocusedViewContext.notEqualsTo('')); +// --- Reset View Location with Command +export class ResetFocusedViewLocationAction extends Action { + static readonly ID = 'workbench.action.resetFocusedViewLocation'; + static readonly LABEL = nls.localize('resetFocusedViewLocation', "Reset Focused View Location"); + + constructor( + id: string, + label: string, + @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, + @IContextKeyService private contextKeyService: IContextKeyService, + @INotificationService private notificationService: INotificationService, + @IViewsService private viewsService: IViewsService + ) { + super(id, label); + } + + async run(): Promise { + const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + + let viewDescriptor: IViewDescriptor | null = null; + if (focusedViewId !== undefined && focusedViewId.trim() !== '') { + viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedViewId); + } + + if (!viewDescriptor) { + this.notificationService.error(nls.localize('resetFocusedView.error.noFocusedView', "There is no view currently focused.")); + return; + } + + const defaultContainer = this.viewDescriptorService.getDefaultContainer(viewDescriptor.id); + if (!defaultContainer || defaultContainer === this.viewDescriptorService.getViewContainer(viewDescriptor.id)) { + return; + } + + this.viewDescriptorService.moveViewsToContainer([viewDescriptor], defaultContainer); + this.viewsService.openView(viewDescriptor.id, true); + + } +} + +registry.registerWorkbenchAction(SyncActionDescriptor.create(ResetFocusedViewLocationAction, ResetFocusedViewLocationAction.ID, ResetFocusedViewLocationAction.LABEL), 'View: Reset Focused View Location', viewCategory, FocusedViewContext.notEqualsTo('')); + + // --- Resize View export abstract class BaseResizeViewAction extends Action { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 5bd47e8dbc..2249d282db 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -39,6 +39,21 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; + +interface IPlaceholderViewlet { + id: string; + name?: string; + iconUrl?: UriComponents; + views?: { when?: string }[]; +} + +interface IPinnedViewlet { + id: string; + pinned: boolean; + order?: number; + visible: boolean; +} interface ICachedViewlet { id: string; @@ -55,7 +70,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { _serviceBrand: undefined; private static readonly ACTION_HEIGHT = 48; - private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets'; + private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets2'; + private static readonly PLACEHOLDER_VIEWLETS = 'workbench.activity.placeholderViewlets'; //#region IView @@ -92,9 +108,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); + this.migrateFromOldCachedViewletsValue(); + storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.PINNED_VIEWLETS, version: 1 }); this.cachedViewlets = this.getCachedViewlets(); for (const cachedViewlet of this.cachedViewlets) { @@ -528,10 +547,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.compositeBar.layout(new Dimension(width, availableHeight)); } + private getViewContainer(viewletId: string): ViewContainer | undefined { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + return viewContainerRegistry.get(viewletId); + } + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { if (e.key === ActivitybarPart.PINNED_VIEWLETS && e.scope === StorageScope.GLOBAL - && this.cachedViewletsValue !== this.getStoredCachedViewletsValue() /* This checks if current window changed the value or not */) { - this._cachedViewletsValue = undefined; + && this.pinnedViewletsValue !== this.getStoredPinnedViewletsValue() /* This checks if current window changed the value or not */) { + this._pinnedViewletsValue = undefined; const newCompositeItems: ICompositeBarItem[] = []; const compositeItems = this.compositeBar.getCompositeBarItems(); const cachedViewlets = this.getCachedViewlets(); @@ -583,64 +607,88 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } - this.cachedViewletsValue = JSON.stringify(state); + this.storeCachedViewletsState(state); } private getCachedViewlets(): ICachedViewlet[] { - const storedStates: Array = JSON.parse(this.cachedViewletsValue); - const cachedViewlets = storedStates.map(c => { - const serialized: ICachedViewlet = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: undefined, visible: true, name: undefined, iconUrl: undefined, views: undefined } : c; - serialized.visible = isUndefinedOrNull(serialized.visible) ? true : serialized.visible; - return serialized; - }); - - for (const old of this.loadOldCachedViewlets()) { - const cachedViewlet = cachedViewlets.filter(cached => cached.id === old.id)[0]; + const cachedViewlets: Array = JSON.parse(this.pinnedViewletsValue); + for (const placeholderViewlet of JSON.parse(this.placeholderViewletsValue)) { + const cachedViewlet = cachedViewlets.filter(cached => cached.id === placeholderViewlet.id)[0]; if (cachedViewlet) { - cachedViewlet.name = old.name; - cachedViewlet.iconUrl = old.iconUrl; - cachedViewlet.views = old.views; + cachedViewlet.name = placeholderViewlet.name; + cachedViewlet.iconUrl = placeholderViewlet.iconUrl; + cachedViewlet.views = placeholderViewlet.views; } } return cachedViewlets; } - private loadOldCachedViewlets(): ICachedViewlet[] { - const previousState = this.storageService.get('workbench.activity.placeholderViewlets', StorageScope.GLOBAL, '[]'); - const result: ICachedViewlet[] = JSON.parse(previousState); - this.storageService.remove('workbench.activity.placeholderViewlets', StorageScope.GLOBAL); - - return result; + private storeCachedViewletsState(cachedViewlets: ICachedViewlet[]): void { + this.pinnedViewletsValue = JSON.stringify(cachedViewlets.map(({ id, pinned, visible, order }) => ({ id, pinned, visible, order }))); + this.placeholderViewletsValue = JSON.stringify(cachedViewlets.map(({ id, iconUrl, name, views }) => ({ id, iconUrl, name, views }))); } - private _cachedViewletsValue: string | undefined; - private get cachedViewletsValue(): string { - if (!this._cachedViewletsValue) { - this._cachedViewletsValue = this.getStoredCachedViewletsValue(); + private _pinnedViewletsValue: string | undefined; + private get pinnedViewletsValue(): string { + if (!this._pinnedViewletsValue) { + this._pinnedViewletsValue = this.getStoredPinnedViewletsValue(); } - return this._cachedViewletsValue; + return this._pinnedViewletsValue; } - private set cachedViewletsValue(cachedViewletsValue: string) { - if (this.cachedViewletsValue !== cachedViewletsValue) { - this._cachedViewletsValue = cachedViewletsValue; - this.setStoredCachedViewletsValue(cachedViewletsValue); + private set pinnedViewletsValue(pinnedViewletsValue: string) { + if (this.pinnedViewletsValue !== pinnedViewletsValue) { + this._pinnedViewletsValue = pinnedViewletsValue; + this.setStoredPinnedViewletsValue(pinnedViewletsValue); } } - private getStoredCachedViewletsValue(): string { + private getStoredPinnedViewletsValue(): string { return this.storageService.get(ActivitybarPart.PINNED_VIEWLETS, StorageScope.GLOBAL, '[]'); } - private setStoredCachedViewletsValue(value: string): void { + private setStoredPinnedViewletsValue(value: string): void { this.storageService.store(ActivitybarPart.PINNED_VIEWLETS, value, StorageScope.GLOBAL); } - private getViewContainer(viewletId: string): ViewContainer | undefined { - const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - return viewContainerRegistry.get(viewletId); + private _placeholderViewletsValue: string | undefined; + private get placeholderViewletsValue(): string { + if (!this._placeholderViewletsValue) { + this._placeholderViewletsValue = this.getStoredPlaceholderViewletsValue(); + } + + return this._placeholderViewletsValue; + } + + private set placeholderViewletsValue(placeholderViewletsValue: string) { + if (this.placeholderViewletsValue !== placeholderViewletsValue) { + this._placeholderViewletsValue = placeholderViewletsValue; + this.setStoredPlaceholderViewletsValue(placeholderViewletsValue); + } + } + + private getStoredPlaceholderViewletsValue(): string { + return this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEWLETS, StorageScope.GLOBAL, '[]'); + } + + private setStoredPlaceholderViewletsValue(value: string): void { + this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEWLETS, value, StorageScope.GLOBAL); + } + + private migrateFromOldCachedViewletsValue(): void { + const value = this.storageService.get('workbench.activity.pinnedViewlets', StorageScope.GLOBAL); + if (value !== undefined) { + const storedStates: Array = JSON.parse(value); + const cachedViewlets = storedStates.map(c => { + const serialized: ICachedViewlet = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: undefined, visible: true, name: undefined, iconUrl: undefined, views: undefined } : c; + serialized.visible = isUndefinedOrNull(serialized.visible) ? true : serialized.visible; + return serialized; + }); + this.storeCachedViewletsState(cachedViewlets); + this.storageService.remove('workbench.activity.pinnedViewlets', StorageScope.GLOBAL); + } } toJSON(): object { diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index d2c4866b03..e901ea537d 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -133,9 +133,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... single view - const defaultContainer = this.viewDescriptorService.getDefaultContainer(draggedViews[0].id); - const canMoveToDefault = !!defaultContainer && this.viewDescriptorService.getViewContainerLocation(defaultContainer) === this.targetContainerLocation; - return !!draggedViews[0].canMoveView && (!!draggedViews[0].containerIcon || canMoveToDefault || this.targetContainerLocation === ViewContainerLocation.Panel); + return !!draggedViews[0].canMoveView; } else { // Dragging an individual view const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dragData.id); @@ -146,7 +144,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... to create a view container - return this.targetContainerLocation === ViewContainerLocation.Panel || !!viewDescriptor.containerIcon; + return true; } } } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1a8bc3867b..6e911a75ca 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -36,7 +36,9 @@ import { SplitEditorUpAction, SplitEditorDownAction, MoveEditorToLeftGroupAction, MoveEditorToRightGroupAction, MoveEditorToAboveGroupAction, MoveEditorToBelowGroupAction, CloseAllEditorGroupsAction, JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, - NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, QuickOpenPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickOpenNextRecentlyUsedEditorAction as QuickOpenLeastRecentlyUsedEditorAction, QuickOpenLeastRecentlyUsedEditorInGroupAction + NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, + QuickOpenPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickOpenNextRecentlyUsedEditorAction as QuickOpenLeastRecentlyUsedEditorAction, + QuickOpenLeastRecentlyUsedEditorInGroupAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index ff72f343b7..b63024ab3c 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -24,6 +24,7 @@ import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { values } from 'vs/base/common/map'; +import { ItemActivation } from 'vs/platform/quickinput/common/quickInput'; export class ExecuteCommandAction extends Action { @@ -1261,6 +1262,7 @@ export class BaseQuickOpenEditorAction extends Action { id: string, label: string, private prefix: string, + private itemActivation: ItemActivation | undefined, @IQuickOpenService private readonly quickOpenService: IQuickOpenService, @IKeybindingService private readonly keybindingService: IKeybindingService ) { @@ -1270,7 +1272,10 @@ export class BaseQuickOpenEditorAction extends Action { async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); - this.quickOpenService.show(this.prefix, { quickNavigateConfiguration: { keybindings } }); + this.quickOpenService.show(this.prefix, { + quickNavigateConfiguration: { keybindings }, + autoFocus: this.itemActivation === ItemActivation.LAST ? { autoFocusLastEntry: true } : undefined + }); } } @@ -1285,7 +1290,7 @@ export class QuickOpenPreviousRecentlyUsedEditorAction extends BaseQuickOpenEdit @IQuickOpenService quickOpenService: IQuickOpenService, @IKeybindingService keybindingService: IKeybindingService ) { - super(id, label, NAVIGATE_ALL_EDITORS_BY_MOST_RECENTLY_USED_PREFIX, quickOpenService, keybindingService); + super(id, label, NAVIGATE_ALL_EDITORS_BY_MOST_RECENTLY_USED_PREFIX, undefined, quickOpenService, keybindingService); } } @@ -1300,7 +1305,7 @@ export class QuickOpenNextRecentlyUsedEditorAction extends BaseQuickOpenEditorAc @IQuickOpenService quickOpenService: IQuickOpenService, @IKeybindingService keybindingService: IKeybindingService ) { - super(id, label, NAVIGATE_ALL_EDITORS_BY_MOST_RECENTLY_USED_PREFIX, quickOpenService, keybindingService); + super(id, label, NAVIGATE_ALL_EDITORS_BY_MOST_RECENTLY_USED_PREFIX, undefined, quickOpenService, keybindingService); } } @@ -1315,7 +1320,7 @@ export class QuickOpenPreviousRecentlyUsedEditorInGroupAction extends BaseQuickO @IQuickOpenService quickOpenService: IQuickOpenService, @IKeybindingService keybindingService: IKeybindingService ) { - super(id, label, NAVIGATE_IN_ACTIVE_GROUP_BY_MOST_RECENTLY_USED_PREFIX, quickOpenService, keybindingService); + super(id, label, NAVIGATE_IN_ACTIVE_GROUP_BY_MOST_RECENTLY_USED_PREFIX, undefined, quickOpenService, keybindingService); } } @@ -1330,7 +1335,7 @@ export class QuickOpenLeastRecentlyUsedEditorInGroupAction extends BaseQuickOpen @IQuickOpenService quickOpenService: IQuickOpenService, @IKeybindingService keybindingService: IKeybindingService ) { - super(id, label, NAVIGATE_IN_ACTIVE_GROUP_BY_MOST_RECENTLY_USED_PREFIX, quickOpenService, keybindingService); + super(id, label, NAVIGATE_IN_ACTIVE_GROUP_BY_MOST_RECENTLY_USED_PREFIX, ItemActivation.LAST, quickOpenService, keybindingService); } } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index eea601ec4b..5fd07baa98 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -37,6 +37,7 @@ import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExte import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; interface ICachedPanel { id: string; @@ -104,6 +105,7 @@ export class PanelPart extends CompositePart implements IPanelService { @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionService private readonly extensionService: IExtensionService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super( notificationService, @@ -125,6 +127,7 @@ export class PanelPart extends CompositePart implements IPanelService { ); this.panelRegistry = Registry.as(PanelExtensions.Panels); + storageKeysSyncRegistryService.registerStorageKey({ key: PanelPart.PINNED_PANELS, version: 1 }); this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { icon: false, diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 9ea8145d9c..8184340ffb 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -47,7 +47,7 @@ import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; import { timeout } from 'vs/base/common/async'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -179,7 +179,20 @@ export class QuickOpenController extends Component implements IQuickOpenService show(prefix?: string, options?: IShowOptions): Promise { if (this.useNewExperimentalVersion) { - this.quickInputService.quickAccess.show(prefix, options); + this.quickInputService.quickAccess.show(prefix, { + quickNavigateConfiguration: options?.quickNavigateConfiguration, + itemActivation: (() => { + if (options?.autoFocus?.autoFocusSecondEntry) { + return ItemActivation.SECOND; + } + + if (options?.autoFocus?.autoFocusLastEntry) { + return ItemActivation.LAST; + } + + return undefined; + })() + }); return Promise.resolve(); } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index b24676f0ac..25dbf8a363 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -21,8 +21,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; -import { IThemeService, IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER, SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom'; @@ -333,23 +333,6 @@ class FocusSideBarAction extends Action { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - - // Sidebar Background: since views can host editors, we apply a background rule if the sidebar background - // color is different from the editor background color. This is a bit of a hack though. The better way - // would be to have a way to push the background color onto each editor widget itself somehow. - const sidebarBackground = theme.getColor(SIDE_BAR_BACKGROUND); - if (sidebarBackground && sidebarBackground !== theme.getColor(editorBackground)) { - collector.addRule(` - .monaco-workbench .part.sidebar > .content .monaco-editor, - .monaco-workbench .part.sidebar > .content .monaco-editor .margin, - .monaco-workbench .part.sidebar > .content .monaco-editor .monaco-editor-background { - background-color: ${sidebarBackground}; - } - `); - } -}); - const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusSideBarAction, FocusSideBarAction.ID, FocusSideBarAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_0 diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 0b97995b85..e15a06d0ab 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -35,6 +35,7 @@ import { values } from 'vs/base/common/map'; import { assertIsDefined } from 'vs/base/common/types'; import { Emitter, Event } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; interface IPendingStatusbarEntry { id: string; @@ -55,7 +56,7 @@ interface IStatusbarViewModelEntry { class StatusbarViewModel extends Disposable { - private static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden'; + static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden'; private readonly _entries: IStatusbarViewModelEntry[] = []; get entries(): IStatusbarViewModelEntry[] { return this._entries; } @@ -354,12 +355,14 @@ export class StatusbarPart extends Part implements IStatusbarService { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IContextMenuService private contextMenuService: IContextMenuService + @IContextMenuService private contextMenuService: IContextMenuService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.viewModel = this._register(new StatusbarViewModel(storageService)); this.onDidChangeEntryVisibility = this.viewModel.onDidChangeEntryVisibility; + storageKeysSyncRegistryService.registerStorageKey({ key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, version: 1 }); this.registerListeners(); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 78a722ee63..6c74dd0d26 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -467,7 +467,7 @@ export class TitlebarPart extends Part implements ITitleService { if ((!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden') { this.title.style.zoom = `${1 / getZoomFactor()}`; } else { - this.title.style.zoom = null; + this.title.style.zoom = ''; } runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index c5f9cbf4e3..7784c5e0c9 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -414,7 +414,7 @@ export class TreeView extends Disposable implements ITreeView { const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); const aligner = new Aligner(this.themeService); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); @@ -1015,7 +1015,7 @@ export class CustomTreeView extends TreeView { private activate() { if (!this.activated) { - this.progressService.withProgress({ location: this.viewContainer.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => { this.updateMessage(); diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 3ff9961bbe..5327a3cb5a 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -22,7 +22,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { VIEW_ID as SEARCH_VIEW_ID } from 'vs/workbench/services/search/common/search'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { PaneCompositePanel, PanelRegistry, PanelDescriptor, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -571,33 +570,28 @@ export class ViewsService extends Disposable implements IViewsService { } })); - const newLocation = location === ViewContainerLocation.Panel ? ViewContainerLocation.Sidebar : ViewContainerLocation.Panel; - disposables.add(registerAction2(class MoveViewAction extends Action2 { + disposables.add(registerAction2(class ResetViewLocationAction extends Action2 { constructor() { super({ - id: `${viewDescriptor.id}.moveView`, + id: `${viewDescriptor.id}.resetViewLocation`, title: { - original: newLocation === ViewContainerLocation.Sidebar ? 'Move to Sidebar' : 'Move to Panel', - value: newLocation === ViewContainerLocation.Sidebar ? localize('moveViewToSidebar', "Move to Sidebar") : localize('moveViewToPanel', "Move to Panel") + original: 'Reset View Location', + value: localize('resetViewLocation', "Reset View Location") }, menu: [{ id: MenuId.ViewTitleContext, when: ContextKeyExpr.or( ContextKeyExpr.and( ContextKeyExpr.equals('view', viewDescriptor.id), - ContextKeyExpr.has(`${viewDescriptor.id}.canMove`), - ContextKeyExpr.equals('config.workbench.view.experimental.allowMovingToNewContainer', true)), - ContextKeyExpr.and( - ContextKeyExpr.equals('view', viewDescriptor.id), - ContextKeyExpr.has(`${viewDescriptor.id}.canMove`), - ContextKeyExpr.equals('view', SEARCH_VIEW_ID) + ContextKeyExpr.equals(`${viewDescriptor.id}.defaultViewLocation`, false) ) ) }], }); } run(accessor: ServicesAccessor): void { - accessor.get(IViewDescriptorService).moveViewToLocation(viewDescriptor, newLocation); + const viewDescriptorService = accessor.get(IViewDescriptorService); + viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getDefaultContainer(viewDescriptor.id)!); accessor.get(IViewsService).openView(viewDescriptor.id, true); } })); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 0da4b0aa11..577057aafc 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -226,11 +226,6 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'default': true, // {{SQL CARBON EDIT}} - change the default value from false to true. 'description': nls.localize('viewVisibility', "Controls the visibility of view header actions. View header actions may either be always visible, or only visible when that view is focused or hovered over.") }, - 'workbench.view.experimental.allowMovingToNewContainer': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('movingViewContainer', "Controls whether specific views will have a context menu entry allowing them to be moved to a new container. Currently, this setting only affects the outline view and views contributed by extensions.") - }, 'workbench.fontAliasing': { 'type': 'string', 'enum': ['default', 'antialiased', 'none', 'auto'], diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 3376773299..ee5aed4ff5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -20,7 +20,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { IColorMapping } from 'vs/platform/theme/common/styler'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_TITLE = 'Comments'; @@ -149,10 +149,15 @@ export class CommentNodeRenderer implements IListRenderer } } +export interface ICommentsListOptions { + overrideStyles?: IColorMapping; +} + export class CommentsList extends WorkbenchAsyncDataTree { constructor( labels: ResourceLabels, container: HTMLElement, + options: ICommentsListOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -202,9 +207,7 @@ export class CommentsList extends WorkbenchAsyncDataTree { collapseByDefault: () => { return false; }, - overrideStyles: { - listBackground: PANEL_BACKGROUND - } + overrideStyles: options.overrideStyles }, contextKeyService, listService, diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5b64ae3f28..f16daca921 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -149,7 +149,7 @@ export class CommentsPanel extends ViewPane { private createTree(): void { this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer)); + this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, { overrideStyles: { listBackground: this.getBackgroundColor() } })); const commentsNavigator = this._register(ResourceNavigator.createTreeResourceNavigator(this.tree, { openOnFocus: true })); this._register(commentsNavigator.onDidOpenResource(e => { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index cc12a8865f..7e8cb26b6d 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -13,7 +13,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorActivation, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -21,16 +21,16 @@ import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, EditorOptions, IEditorInput, IEditorPane, GroupIdentifier } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { CustomEditorInput } from './customEditorInput'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; export const defaultEditorId = 'default'; @@ -284,6 +284,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ): Promise { const targetGroup = group || this.editorGroupService.activeGroup; + if (options && typeof options.activation === 'undefined') { + options = { ...options, activation: options.preserveFocus ? EditorActivation.RESTORE : undefined }; + } + // Try to replace existing editors for resource const existingEditors = targetGroup.editors.filter(editor => editor.resource && isEqual(editor.resource, resource)); if (existingEditors.length) { @@ -454,7 +458,11 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.resource)); if (existingEditorForResource) { return { - override: this.editorService.openEditor(existingEditorForResource, { ...options, ignoreOverrides: true }, group) + override: this.editorService.openEditor(existingEditorForResource, { + ...options, + ignoreOverrides: true, + activation: options?.preserveFocus ? EditorActivation.RESTORE : undefined, + }, group) }; } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 2c23b503f1..b0d12ae1cb 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -96,6 +96,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewE id: OpenDebugPanelAction.ID, keybindings: openPanelKb }, + order: 3, hideIfEmpty: true }, ViewContainerLocation.Panel); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index b883a6e8f8..f2a5d989cb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -234,7 +234,7 @@ export class DebugSession implements IDebugSession { this.initialized = true; this._onDidChangeState.fire(); - this.model.setExceptionBreakpoints(this.raw!.capabilities.exceptionBreakpointFilters || []); + this.model.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 57191d3742..7a0e69967c 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -208,9 +208,20 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { })); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { - this.onDidFontChange(); + this.onDidStyleChange(); } })); + + this._register(this.themeService.onDidColorThemeChange(e => { + this.onDidStyleChange(); + })); + + this._register(this.viewDescriptorService.onDidChangeLocation(e => { + if (e.views.some(v => v.id === this.id)) { + this.onDidStyleChange(); + } + })); + this._register(this.editorService.onDidActiveEditorChange(() => { this.setMode(); })); @@ -253,14 +264,15 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } } - private onDidFontChange(): void { + private onDidStyleChange(): void { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; const fontSize = debugConsole.fontSize; const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : debugConsole.fontFamily; const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; + const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); - // Set the font size, font family, line height and align the twistie to be centered + // Set the font size, font family, line height and align the twistie to be centered, and input theme color this.styleElement.innerHTML = ` .repl .repl-tree .expression { font-size: ${fontSize}px; @@ -274,6 +286,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { .repl .repl-tree .monaco-tl-twistie { background-position-y: calc(100% - ${fontSize * 1.4 / 2 - 8}px); } + + .repl .repl-input-wrapper .monaco-editor .lines-content { + background-color: ${backgroundColor}; + } `; this.tree.rerender(); @@ -510,7 +526,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // Make sure to select the session if debugging is already active this.selectSession(); this.styleElement = dom.createStyleSheet(this.container); - this.onDidFontChange(); + this.onDidStyleChange(); } private createReplInput(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index e2f0a2a201..59d78151a5 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -80,8 +80,11 @@ export class VariablesView extends ViewPane { if (stackFrame) { const scopes = await stackFrame.getScopes(); // Expand the first scope if it is not expensive and if there is no expansion state (all are collapsed) - if (scopes.every(s => this.tree.getNode(s).collapsed) && scopes.length > 0 && !scopes[0].expensive) { - this.tree.expand(scopes[0]); + if (scopes.every(s => this.tree.getNode(s).collapsed) && scopes.length > 0) { + const toExpand = scopes.filter(s => !s.expensive).shift(); + if (toExpand) { + this.tree.expand(toExpand); + } } } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index ae1c9a5392..eed08b1b84 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -305,9 +305,19 @@ export class StackFrame implements IStackFrame { getScopes(): Promise { if (!this.scopes) { this.scopes = this.thread.session.scopes(this.frameId, this.thread.threadId).then(response => { - return response && response.body && response.body.scopes ? - response.body.scopes.map((rs, index) => new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, - rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : undefined)) : []; + if (!response || !response.body || !response.body.scopes) { + return []; + } + + const scopeNameIndexes = new Map(); + return response.body.scopes.map(rs => { + const previousIndex = scopeNameIndexes.get(rs.name); + const index = typeof previousIndex === 'number' ? previousIndex + 1 : 0; + scopeNameIndexes.set(rs.name, index); + return new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, + rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : undefined); + + }); }, err => [new ErrorScope(this, 0, err.message)]); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index c9fe7aa36e..8d26cb405e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -48,12 +48,10 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid } // Extension name typed: offer to search it - else { - return [genericSearchPickItem]; - } + return [genericSearchPickItem]; } - protected async getPicksForExtensionId(filter: string, fallback: IPickerQuickAccessItem, token: CancellationToken): Promise> { + private async getPicksForExtensionId(filter: string, fallback: IPickerQuickAccessItem, token: CancellationToken): Promise> { try { const galleryResult = await this.galleryService.query({ names: [filter], pageSize: 1 }, token); if (token.isCancellationRequested) { @@ -63,12 +61,12 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid const galleryExtension = galleryResult.firstPage[0]; if (!galleryExtension) { return [fallback]; - } else { - return [{ - label: localize('install', "Press Enter to install extension '{0}'.", filter), - accept: () => this.installExtension(galleryExtension, filter) - }]; } + + return [{ + label: localize('install', "Press Enter to install extension '{0}'.", filter), + accept: () => this.installExtension(galleryExtension, filter) + }]; } catch (error) { if (token.isCancellationRequested) { return []; // expected error diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index c215e2e00a..8735b95ad3 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -46,6 +46,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { sequence } from 'vs/base/common/async'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { once } from 'vs/base/common/functional'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -519,9 +520,10 @@ export class GlobalCompareResourcesAction extends Action { return undefined; }); + once(this.quickOpenService.onHide)((() => toDispose.dispose())); + // Bring up quick open await this.quickOpenService.show('', { autoFocus: { autoFocusSecondEntry: true } }); - toDispose.dispose(); // make sure to unbind if quick open is closing } else { this.notificationService.info(nls.localize('openFileToCompare', "Open a file first to compare it with another file.")); } diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index aefa8d1887..adf2ca063d 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -112,6 +112,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC id: Constants.MARKERS_CONTAINER_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, hideIfEmpty: true, + order: 0, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, Constants.MARKERS_VIEW_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), focusCommand: { id: ToggleMarkersPanelAction.ID, keybindings: { @@ -122,6 +123,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ id: Constants.MARKERS_VIEW_ID, + containerIcon: 'codicon-warning', name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, canToggleVisibility: false, canMoveView: true, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index a865aebce6..fb4578a154 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -201,6 +201,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { mouseSupport: true, multipleSelectionSupport: false, enableKeyboardNavigation: true, + additionalScrollHeight: 0, overrideStyles: { listBackground: editorBackground, listActiveSelectionBackground: editorBackground, @@ -419,6 +420,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600); DOM.size(this.body, dimension.width, dimension.height); + this.list?.updateOptions({ additionalScrollHeight: dimension.height }); this.list?.layout(dimension.height, dimension.width); this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } @@ -742,6 +744,13 @@ export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator hc: new Color(new RGBA(0, 73, 122)) }, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { + dark: new Color(new RGBA(255, 255, 255, 0.06)), + light: new Color(new RGBA(228, 230, 241)), + hc: null +} + , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); + registerThemingParticipant((theme, collector) => { const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); @@ -776,10 +785,10 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`); } - const inactiveListItem = theme.getColor('list.inactiveSelectionBackground'); + const containerBackground = theme.getColor(notebookOutputContainerColor); - if (inactiveListItem) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${inactiveListItem}; }`); + if (containerBackground) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${containerBackground}; }`); } const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index d18cedd2d8..9af683defb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -94,9 +94,13 @@ abstract class AbstractCellRenderer { horizontalHasArrows: false, alwaysConsumeMouseWheel: false }, - overviewRulerLanes: 3, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, fixedOverflowWidgets: false, - lineNumbersMinChars: 1, minimap: { enabled: false }, }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 835da45ab3..d18e402070 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -15,6 +15,7 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { IModeService } from 'vs/editor/common/services/modeService'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -28,7 +29,8 @@ export class CodeCell extends Disposable { private viewCell: CodeCellViewModel, private templateData: CellRenderTemplate, @INotebookService private notebookService: INotebookService, - @IQuickInputService private readonly quickInputService: IQuickInputService + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IModeService private readonly _modeService: IModeService ) { super(); @@ -85,6 +87,11 @@ export class CodeCell extends Disposable { templateData.editor?.updateOptions({ readOnly: !(viewCell.getEvaluatedMetadata(notebookEditor.viewModel?.metadata).editable) }); })); + this._register(viewCell.onDidChangeLanguage((e) => { + const mode = this._modeService.create(e); + templateData.editor?.getModel()?.setMode(mode.languageIdentifier); + })); + let cellWidthResizeObserver = getResizesObserver(templateData.editorContainer!, { width: width, height: totalHeight diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 182dd1c15e..8c314114d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -34,6 +34,8 @@ export abstract class BaseCellViewModel extends Disposable { public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; protected readonly _onDidChangeMetadata: Emitter = this._register(new Emitter()); public readonly onDidChangeMetadata: Event = this._onDidChangeMetadata.event; + protected readonly _onDidChangeLanguage: Emitter = this._register(new Emitter()); + public readonly onDidChangeLanguage: Event = this._onDidChangeLanguage.event; get handle() { return this.cell.handle; } @@ -100,6 +102,10 @@ export abstract class BaseCellViewModel extends Disposable { constructor(readonly viewType: string, readonly notebookHandle: number, readonly cell: ICell, public id: string) { super(); + this._register(cell.onDidChangeLanguage((e) => { + this._onDidChangeLanguage.fire(e); + })); + this._register(cell.onDidChangeMetadata((e) => { this._onDidChangeMetadata.fire(e); })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 140caec0ec..a7e158ea40 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -89,6 +89,12 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo }); } })); + + this._register(this.onDidChangeLanguage((e) => { + if (this._textModel && !this._textModel.isDisposed()) { + + } + })); } layoutChange(state: CodeCellLayoutChangeEvent) { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index faab29962e..9eca161cc9 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -18,6 +18,9 @@ export class NotebookCellTextModel implements ICell { private _onDidChangeMetadata = new Emitter(); onDidChangeMetadata: Event = this._onDidChangeMetadata.event; + private _onDidChangeLanguage = new Emitter(); + onDidChangeLanguage: Event = this._onDidChangeLanguage.event; + private _outputs: IOutput[]; get outputs(): IOutput[] { @@ -44,13 +47,22 @@ export class NotebookCellTextModel implements ICell { this._onDidChangeMetadata.fire(this._metadata); } + get language() { + return this._language; + } + + set language(newLanguage: string) { + this._language = newLanguage; + this._onDidChangeLanguage.fire(newLanguage); + } + private _buffer: PieceTreeTextBufferFactory | null = null; constructor( readonly uri: URI, public handle: number, private _source: string[], - public language: string, + private _language: string, public cellKind: CellKind, outputs: IOutput[], metadata: NotebookCellMetadata | undefined diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 27aa02eb44..db4766608b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -24,6 +24,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel languages: string[] = []; metadata: NotebookDocumentMetadata | undefined = { editable: true }; renderers = new Set(); + private _isUntitled: boolean | undefined = undefined; constructor( public handle: number, @@ -36,6 +37,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel updateLanguages(languages: string[]) { this.languages = languages; + + // TODO@rebornix metadata: default language for cell + if (this._isUntitled && languages.length && this.cells.length) { + this.cells[0].language = languages[0]; + } } updateNotebookMetadata(metadata: NotebookDocumentMetadata) { @@ -57,7 +63,27 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }); } + insertTemplateCell(cell: NotebookCellTextModel) { + if (this.cells.length > 0 || this._isUntitled !== undefined) { + return; + } + + this._isUntitled = true; + this.cells = [cell]; + + let dirtyStateListener = cell.onDidChangeContent(() => { + this._isUntitled = false; + this._onDidChangeContent.fire(); + }); + + this._cellListeners.set(cell.handle, dirtyStateListener); + this._onDidChangeContent.fire(); + return; + } + insertNewCell(index: number, cell: NotebookCellTextModel): void { + this._isUntitled = false; + this._mapping.set(cell.handle, cell); this.cells.splice(index, 0, cell); let dirtyStateListener = cell.onDidChangeContent(() => { @@ -70,6 +96,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } removeCell(index: number) { + this._isUntitled = false; + let cell = this.cells[index]; this._cellListeners.get(cell.handle)?.dispose(); this._cellListeners.delete(cell.handle); @@ -80,6 +108,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // TODO@rebornix should this trigger content change event? $spliceNotebookCells(splices: NotebookCellsSplice[]): void { + if (!splices.length) { + return; + } + + this._isUntitled = false; + splices.reverse().forEach(splice => { let cellDtos = splice[2]; let newCells = cellDtos.map(cell => { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 98ebf2cdf9..28e449287f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -136,6 +136,7 @@ export interface ICell { metadata?: NotebookCellMetadata; onDidChangeOutputs?: Event; onDidChangeMetadata: Event; + onDidChangeLanguage: Event; resolveTextBufferFactory(): PieceTreeTextBufferFactory; // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel contentChange(): void; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index f5b062aa6a..c74e43a2d7 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -27,6 +27,9 @@ export class TestCell implements ICell { onDidChangeOutputs: Event = this._onDidChangeOutputs.event; private _onDidChangeMetadata = new Emitter(); onDidChangeMetadata: Event = this._onDidChangeMetadata.event; + private _onDidChangeLanguage = new Emitter(); + onDidChangeLanguage: Event = this._onDidChangeLanguage.event; + private _isDirty: boolean = false; private _outputs: IOutput[]; diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index c395fc6c06..952a1b518f 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -61,6 +61,7 @@ const toggleOutputActionKeybindings = { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: OUTPUT_VIEW_ID, name: nls.localize('output', "Output"), + order: 1, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [OUTPUT_VIEW_ID, OUTPUT_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), focusCommand: { id: toggleOutputAcitonId, keybindings: toggleOutputActionKeybindings } }, ViewContainerLocation.Panel); diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css similarity index 100% rename from src/vs/workbench/contrib/remote/browser/remoteViewlet.css rename to src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 88aa48e755..9f4d00b7e1 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./remoteViewlet'; +import 'vs/css!./media/remoteViewlet'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; @@ -55,6 +55,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Event } from 'vs/base/common/event'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { RemoteWindowActiveIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; export interface HelpInformation { extensionDescription: IExtensionDescription; @@ -803,3 +804,5 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting); + diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts new file mode 100644 index 0000000000..75c30ef8fb --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { RemoteConnectionState, Deprecated_RemoteAuthorityContext } from 'vs/workbench/browser/contextkeys'; +import { isWeb } from 'vs/base/common/platform'; +import { once } from 'vs/base/common/functional'; + +const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; +const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; +const SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command + +export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution { + + private windowIndicatorEntry: IStatusbarEntryAccessor | undefined; + private windowCommandMenu: IMenu; + private hasWindowActions: boolean = false; + private remoteAuthority: string | undefined; + private connectionState: 'initializing' | 'connected' | 'disconnected' | undefined = undefined; + + constructor( + @IStatusbarService private readonly statusbarService: IStatusbarService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @ILabelService private readonly labelService: ILabelService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IMenuService private menuService: IMenuService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @ICommandService private readonly commandService: ICommandService, + @IExtensionService extensionService: IExtensionService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IHostService hostService: IHostService + ) { + super(); + + this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService); + this._register(this.windowCommandMenu); + + const category = nls.localize('remote.category', "Remote"); + const that = this; + registerAction2(class extends Action2 { + constructor() { + super({ + id: WINDOW_ACTIONS_COMMAND_ID, + category, + title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, + f1: true, + }); + } + run = () => that.showIndicatorActions(that.windowCommandMenu); + }); + + this.remoteAuthority = environmentService.configuration.remoteAuthority; + Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); + + if (this.remoteAuthority) { + + if (SHOW_CLOSE_REMOTE_COMMAND_ID) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: CLOSE_REMOTE_COMMAND_ID, + category, + title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, + f1: true + }); + } + run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CLOSE_REMOTE_COMMAND_ID, + title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") + }, + order: 3.5 + }); + } + + // Pending entry until extensions are ready + this.renderWindowIndicator('$(sync~spin) ' + nls.localize('host.open', "Opening Remote..."), undefined, WINDOW_ACTIONS_COMMAND_ID); + this.connectionState = 'initializing'; + RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); + + const connection = remoteAgentService.getConnection(); + if (connection) { + this._register(connection.onDidStateChange((e) => { + switch (e.type) { + case PersistentConnectionEventType.ConnectionLost: + case PersistentConnectionEventType.ReconnectionPermanentFailure: + case PersistentConnectionEventType.ReconnectionRunning: + case PersistentConnectionEventType.ReconnectionWait: + this.setDisconnected(true); + break; + case PersistentConnectionEventType.ConnectionGain: + this.setDisconnected(false); + break; + } + })); + } + } + + extensionService.whenInstalledExtensionsRegistered().then(_ => { + if (this.remoteAuthority) { + this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator())); + remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true)); + } + this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions())); + this.updateWindowIndicator(); + }); + } + + private setDisconnected(isDisconnected: boolean): void { + const newState = isDisconnected ? 'disconnected' : 'connected'; + if (this.connectionState !== newState) { + this.connectionState = newState; + RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); + Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(isDisconnected ? `disconnected/${this.remoteAuthority!}` : this.remoteAuthority!); + this.updateWindowIndicator(); + } + } + + private updateWindowIndicator(): void { + const windowActionCommand = (this.remoteAuthority || this.windowCommandMenu.getActions().length) ? WINDOW_ACTIONS_COMMAND_ID : undefined; + if (this.remoteAuthority) { + const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority; + if (this.connectionState !== 'disconnected') { + this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand); + } else { + this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel), windowActionCommand); + } + } else { + if (windowActionCommand) { + this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand); + } else if (this.windowIndicatorEntry) { + this.windowIndicatorEntry.dispose(); + this.windowIndicatorEntry = undefined; + } + } + } + + private updateWindowActions() { + const newHasWindowActions = this.windowCommandMenu.getActions().length > 0; + if (newHasWindowActions !== this.hasWindowActions) { + this.hasWindowActions = newHasWindowActions; + this.updateWindowIndicator(); + } + } + + private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { + const properties: IStatusbarEntry = { + backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command + }; + if (this.windowIndicatorEntry) { + this.windowIndicatorEntry.update(properties); + } else { + this.windowIndicatorEntry = this.statusbarService.addEntry(properties, 'status.host', nls.localize('status.host', "Remote Host"), StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); + } + } + + private showIndicatorActions(menu: IMenu) { + + const actions = menu.getActions(); + + const items: (IQuickPickItem | IQuickPickSeparator)[] = []; + for (let actionGroup of actions) { + if (items.length) { + items.push({ type: 'separator' }); + } + for (let action of actionGroup[1]) { + if (action instanceof MenuItemAction) { + let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; + if (action.item.category) { + const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; + label = nls.localize('cat.title', "{0}: {1}", category, label); + } + items.push({ + type: 'item', + id: action.item.id, + label + }); + } + } + } + + if (SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { + if (items.length) { + items.push({ type: 'separator' }); + } + items.push({ + type: 'item', + id: CLOSE_REMOTE_COMMAND_ID, + label: nls.localize('closeRemote.title', 'Close Remote Connection') + }); + } + + const quickPick = this.quickInputService.createQuickPick(); + quickPick.items = items; + quickPick.canSelectMany = false; + once(quickPick.onDidAccept)((_ => { + const selectedItems = quickPick.selectedItems; + if (selectedItems.length === 1) { + this.commandService.executeCommand(selectedItems[0].id!); + } + quickPick.hide(); + })); + quickPick.show(); + } +} diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 73af5d81fb..1db1d29fa7 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -5,23 +5,17 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ILogService } from 'vs/platform/log/common/log'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogChannel } from 'vs/platform/dialogs/electron-browser/dialogIpc'; @@ -34,211 +28,11 @@ import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteA import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { RemoteConnectionState, Deprecated_RemoteAuthorityContext, RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys'; +import { RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys'; import { IDownloadService } from 'vs/platform/download/common/download'; import { OpenLocalFileFolderCommand, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; -const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; -const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; - -export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution { - - private windowIndicatorEntry: IStatusbarEntryAccessor | undefined; - private windowCommandMenu: IMenu; - private hasWindowActions: boolean = false; - private remoteAuthority: string | undefined; - private connectionState: 'initializing' | 'connected' | 'disconnected' | undefined = undefined; - - constructor( - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @ILabelService private readonly labelService: ILabelService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IMenuService private menuService: IMenuService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @ICommandService private readonly commandService: ICommandService, - @IExtensionService extensionService: IExtensionService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @IHostService hostService: IHostService - ) { - super(); - - this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService); - this._register(this.windowCommandMenu); - - const category = nls.localize('remote.category', "Remote"); - const that = this; - registerAction2(class extends Action2 { - constructor() { - super({ - id: WINDOW_ACTIONS_COMMAND_ID, - category, - title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, - f1: true, - }); - } - run = () => that.showIndicatorActions(that.windowCommandMenu); - }); - - this.remoteAuthority = environmentService.configuration.remoteAuthority; - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); - - if (this.remoteAuthority) { - registerAction2(class extends Action2 { - constructor() { - super({ - id: CLOSE_REMOTE_COMMAND_ID, - category, - title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, - f1: true - }); - } - run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); - }); - - // Pending entry until extensions are ready - this.renderWindowIndicator('$(sync~spin) ' + nls.localize('host.open', "Opening Remote..."), undefined, WINDOW_ACTIONS_COMMAND_ID); - this.connectionState = 'initializing'; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: CLOSE_REMOTE_COMMAND_ID, - title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") - }, - order: 3.5 - }); - - const connection = remoteAgentService.getConnection(); - if (connection) { - this._register(connection.onDidStateChange((e) => { - switch (e.type) { - case PersistentConnectionEventType.ConnectionLost: - case PersistentConnectionEventType.ReconnectionPermanentFailure: - case PersistentConnectionEventType.ReconnectionRunning: - case PersistentConnectionEventType.ReconnectionWait: - this.setDisconnected(true); - break; - case PersistentConnectionEventType.ConnectionGain: - this.setDisconnected(false); - break; - } - })); - } - } - - extensionService.whenInstalledExtensionsRegistered().then(_ => { - if (this.remoteAuthority) { - this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator())); - remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true)); - } - this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions())); - this.updateWindowIndicator(); - }); - } - - private setDisconnected(isDisconnected: boolean): void { - const newState = isDisconnected ? 'disconnected' : 'connected'; - if (this.connectionState !== newState) { - this.connectionState = newState; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(isDisconnected ? `disconnected/${this.remoteAuthority!}` : this.remoteAuthority!); - this.updateWindowIndicator(); - } - } - - private updateWindowIndicator(): void { - const windowActionCommand = (this.remoteAuthority || this.windowCommandMenu.getActions().length) ? WINDOW_ACTIONS_COMMAND_ID : undefined; - if (this.remoteAuthority) { - const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority; - if (this.connectionState !== 'disconnected') { - this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand); - } else { - this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel), windowActionCommand); - } - } else { - if (windowActionCommand) { - this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand); - } else if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.dispose(); - this.windowIndicatorEntry = undefined; - } - } - } - - private updateWindowActions() { - const newHasWindowActions = this.windowCommandMenu.getActions().length > 0; - if (newHasWindowActions !== this.hasWindowActions) { - this.hasWindowActions = newHasWindowActions; - this.updateWindowIndicator(); - } - } - - private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { - const properties: IStatusbarEntry = { - backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command - }; - if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.update(properties); - } else { - this.windowIndicatorEntry = this.statusbarService.addEntry(properties, 'status.host', nls.localize('status.host', "Remote Host"), StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); - } - } - - private showIndicatorActions(menu: IMenu) { - - const actions = menu.getActions(); - - const items: (IQuickPickItem | IQuickPickSeparator)[] = []; - for (let actionGroup of actions) { - if (items.length) { - items.push({ type: 'separator' }); - } - for (let action of actionGroup[1]) { - if (action instanceof MenuItemAction) { - let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; - if (action.item.category) { - const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; - label = nls.localize('cat.title', "{0}: {1}", category, label); - } - items.push({ - type: 'item', - id: action.item.id, - label - }); - } - } - } - - if (this.remoteAuthority) { - if (items.length) { - items.push({ type: 'separator' }); - } - items.push({ - type: 'item', - id: CLOSE_REMOTE_COMMAND_ID, - label: nls.localize('closeRemote.title', 'Close Remote Connection') - }); - } - - const quickPick = this.quickInputService.createQuickPick(); - quickPick.items = items; - quickPick.canSelectMany = false; - quickPick.onDidAccept(_ => { - const selectedItems = quickPick.selectedItems; - if (selectedItems.length === 1) { - this.commandService.executeCommand(selectedItems[0].id!); - } - quickPick.hide(); - }); - quickPick.show(); - } -} - class RemoteChannelsContribution implements IWorkbenchContribution { constructor( @@ -366,7 +160,6 @@ const workbenchContributionsRegistry = Registry.as result.resource); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 4ce6e0e165..65232bed19 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -516,7 +516,7 @@ const viewContainer = Registry.as(ViewExtensions.ViewCo order: 1 }, ViewContainerLocation.Sidebar); -const viewDescriptor = { id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: new SyncDescriptor(SearchView), canToggleVisibility: false, canMoveView: true }; +const viewDescriptor = { id: VIEW_ID, containerIcon: 'codicon-search', name: nls.localize('search', "Search"), ctorDescriptor: new SyncDescriptor(SearchView), canToggleVisibility: false, canMoveView: true }; // Register search default location to sidebar Registry.as(ViewExtensions.ViewsRegistry).registerViews([viewDescriptor], viewContainer); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index e2d8fb1ae6..9a08d826f0 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -462,8 +462,8 @@ export class SearchEditor extends BaseTextEditor { _reason: 'searchEditor', extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), maxResults: 10000, - disregardIgnoreFiles: !config.useIgnores, - disregardExcludeSettings: !config.useIgnores, + disregardIgnoreFiles: !config.useIgnores || undefined, + disregardExcludeSettings: !config.useIgnores || undefined, excludePattern: config.excludes, includePattern: config.includes, previewOptions: { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 653dd1025d..bc8f9193cf 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -382,7 +382,8 @@ const VIEW_CONTAINER = Registry.as(ViewContainerExtensi name: nls.localize('terminal', "Terminal"), ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS }, - hideIfEmpty: true + hideIfEmpty: true, + order: 3 }, ViewContainerLocation.Panel); Registry.as(panel.Extensions.Panels).setDefaultPanelId(TERMINAL_VIEW_ID); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index d33ce3287f..79b9529b78 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -695,7 +695,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, { id: SyncResource.GlobalState, label: getSyncAreaLabel(SyncResource.GlobalState), - description: localize('ui state description', "only 'Display Language' for now") }]; } @@ -914,7 +913,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: signInCommand.id, - title: signInCommand.title, + title: localize('sign in 2', "Sync: Sign in to sync (1)"), menu: { group: '5_sync', id: MenuId.GlobalActivity, diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts index c1d9ff1419..406061db2a 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts @@ -7,10 +7,9 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor, ViewIdentifierMap } from './viewsWelcomeExtensionPoint'; +import { ViewsWelcomeExtensionPoint, ViewWelcome, ViewIdentifierMap } from './viewsWelcomeExtensionPoint'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ViewContainerExtensions, IViewsRegistry, ViewContentPriority } from 'vs/workbench/common/views'; -import { localize } from 'vs/nls'; const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -23,11 +22,6 @@ export class ViewsWelcomeContribution extends Disposable implements IWorkbenchCo extensionPoint.setHandler((_, { added, removed }) => { for (const contribution of removed) { - // Proposed API check - if (!contribution.description.enableProposedApi) { - continue; - } - for (const welcome of contribution.value) { const disposable = this.viewWelcomeContents.get(welcome); @@ -38,12 +32,6 @@ export class ViewsWelcomeContribution extends Disposable implements IWorkbenchCo } for (const contribution of added) { - // Proposed API check - if (!contribution.description.enableProposedApi) { - contribution.collector.error(localize('proposedAPI.invalid', "The '{0}' contribution is a proposed API and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", viewsWelcomeExtensionPointDescriptor.extensionPoint, contribution.description.identifier.value)); - continue; - } - for (const welcome of contribution.value) { const id = ViewIdentifierMap[welcome.view] ?? welcome.view; const disposable = viewsRegistry.registerViewWelcomeContent(id, { diff --git a/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts index def2d3d73d..d28af9118f 100644 --- a/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts @@ -219,14 +219,14 @@ export class TitlebarPart extends BrowserTitleBarPart { } } } else { - this.title.style.zoom = null; + this.title.style.zoom = ''; if (isWindows || isLinux) { if (this.appIcon) { - this.appIcon.style.zoom = null; + this.appIcon.style.zoom = ''; } if (this.windowControls) { - this.windowControls.style.zoom = null; + this.windowControls.style.zoom = ''; } } } diff --git a/src/vs/workbench/services/commands/common/commandService.ts b/src/vs/workbench/services/commands/common/commandService.ts index 2faa41e22b..0b2024f284 100644 --- a/src/vs/workbench/services/commands/common/commandService.ts +++ b/src/vs/workbench/services/commands/common/commandService.ts @@ -81,7 +81,7 @@ export class CommandService extends Disposable implements ICommandService { } try { this._onWillExecuteCommand.fire({ commandId: id, args }); - const result = this._instantiationService.invokeFunction.apply(this._instantiationService, [command.handler, ...args]); + const result = this._instantiationService.invokeFunction(command.handler, ...args); this._onDidExecuteCommand.fire({ commandId: id, args }); return Promise.resolve(result); } catch (err) { @@ -90,4 +90,4 @@ export class CommandService extends Disposable implements ICommandService { } } -registerSingleton(ICommandService, CommandService, true); \ No newline at end of file +registerSingleton(ICommandService, CommandService, true); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 3141e9b427..e342f13792 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -339,7 +339,7 @@ export class HistoryService extends Disposable implements IHistoryService { const navigateToStackEntry = this.navigationStack[this.navigationStackIndex]; - this.doNavigate(navigateToStackEntry).finally(() => this.navigatingInStack = false); + this.doNavigate(navigateToStackEntry).finally(() => { this.navigatingInStack = false; }); } private doNavigate(location: IStackEntry): Promise { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 668f47bfb9..766c8103b1 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -150,31 +150,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // update settings schema setting for theme specific settings this.colorThemeRegistry.onDidChange(async event => { updateColorThemeConfigurationSchemas(event.themes); - - const colorThemeSetting = this.settings.colorTheme; - if (colorThemeSetting !== this.currentColorTheme.settingsId) { - const theme = await this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined); - if (theme) { - this.setColorTheme(theme.id, undefined); - return; - } - } - - if (this.currentColorTheme.isLoaded) { - const themeData = await this.colorThemeRegistry.findThemeById(this.currentColorTheme.id); - if (!themeData) { - // current theme is no longer available - prevColorId = this.currentColorTheme.id; - this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto'); - } else { - if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) { - // restore theme - this.setColorTheme(prevColorId, 'auto'); - prevColorId = undefined; - } else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) { - this.reloadCurrentColorTheme(); - } + if (await this.restoreColorTheme()) { // checks if theme from settings exists and is set + // restore theme + if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) { + // restore theme + this.setColorTheme(prevColorId, 'auto'); + prevColorId = undefined; + } else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) { + this.reloadCurrentColorTheme(); } + } else if (event.removed.some(t => t.settingsId === this.currentColorTheme.settingsId)) { + // current theme is no longer available + prevColorId = this.currentColorTheme.id; + this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto'); } }); @@ -189,7 +177,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } else if (event.added.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) { this.reloadCurrentFileIconTheme(); } - } else { + } else if (event.removed.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) { // current theme is no longer available prevFileIconId = this.currentFileIconTheme.id; this.setFileIconTheme(DEFAULT_FILE_ICON_THEME_ID, 'auto'); @@ -208,7 +196,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } else if (event.added.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) { this.reloadCurrentProductIconTheme(); } - } else { + } else if (event.removed.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) { // current theme is no longer available prevProductIconId = this.currentProductIconTheme.id; this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto'); @@ -391,17 +379,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.applyTheme(this.currentColorTheme, undefined, false); } - public restoreColorTheme() { - const colorThemeSetting = this.settings.colorTheme; - if (colorThemeSetting !== this.currentColorTheme.settingsId) { - this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined).then(theme => { - if (theme) { - this.setColorTheme(theme.id, undefined); - } - }); + public async restoreColorTheme(): Promise { + const settingId = this.settings.colorTheme; + const theme = await this.colorThemeRegistry.findThemeBySettingsId(settingId); + if (theme) { + if (settingId !== this.currentColorTheme.settingsId) { + await this.setColorTheme(theme.id, undefined); + } + return true; } + return false; } + private updateDynamicCSSRules(themeData: IColorTheme) { const cssRules = new Set(); const ruleCollector = { diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index eccf5d0b7e..9b22efac75 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -107,6 +107,7 @@ export function registerProductIconThemeExtensionPoint() { export interface ThemeChangeEvent { themes: T[]; added: T[]; + removed: T[]; } export interface IThemeData { @@ -135,10 +136,11 @@ export class ThemeRegistry { private initialize() { this.themesExtPoint.setHandler((extensions, delta) => { - const previousIds: { [key: string]: boolean } = {}; + const previousIds: { [key: string]: T } = {}; + const added: T[] = []; for (const theme of this.extensionThemes) { - previousIds[theme.id] = true; + previousIds[theme.id] = theme; } this.extensionThemes.length = 0; for (let ext of extensions) { @@ -154,9 +156,12 @@ export class ThemeRegistry { for (const theme of this.extensionThemes) { if (!previousIds[theme.id]) { added.push(theme); + } else { + delete previousIds[theme.id]; } } - this.onDidChangeEmitter.fire({ themes: this.extensionThemes, added }); + const removed = Object.values(previousIds); + this.onDidChangeEmitter.fire({ themes: this.extensionThemes, added, removed }); }); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts b/src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts new file mode 100644 index 0000000000..fc8cfb7d06 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; + +class StorageKeysSyncRegistryService extends StorageKeysSyncRegistryChannelClient implements IStorageKeysSyncRegistryService { + + constructor( + @IMainProcessService mainProcessService: IMainProcessService + ) { + super(mainProcessService.getChannel('storageKeysSyncRegistryService')); + } + +} + +registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 57d145ee4c..50da0f6adf 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -14,8 +14,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { firstIndex } from 'vs/base/common/arrays'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { VIEW_ID as SEARCH_VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; class CounterSet implements IReadableSet { @@ -192,6 +191,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor private readonly viewDescriptorCollections: Map; private readonly activeViewContextKeys: Map>; private readonly movableViewContextKeys: Map>; + private readonly defaultViewLocationContextKeys: Map>; private readonly viewsRegistry: IViewsRegistry; private readonly viewContainersRegistry: IViewContainersRegistry; @@ -219,13 +219,15 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); + storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_POSITIONS, version: 1 }); this.viewDescriptorCollections = new Map(); this.activeViewContextKeys = new Map>(); this.movableViewContextKeys = new Map>(); + this.defaultViewLocationContextKeys = new Map>(); this.viewContainersRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); this.viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); @@ -254,33 +256,6 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.storageService.onDidChangeStorage((e) => { this.onDidStorageChange(e); })); this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); - - this._register(this.configurationService.onDidChangeConfiguration((changeEvent) => { - if (changeEvent.affectedKeys.find(key => key === 'workbench.view.experimental.allowMovingToNewContainer')) { - if (this.viewsCanMoveSettingValue) { - return; - } - - // update all moved views to their default locations - for (const viewId of this.cachedViewInfo.keys()) { - if (viewId === SEARCH_VIEW_ID) { - continue; - } - - const viewDescriptor = this.getViewDescriptor(viewId); - const viewLocation = this.getViewContainer(viewId); - const defaultLocation = this.getDefaultContainer(viewId); - - if (viewDescriptor && viewLocation && defaultLocation && defaultLocation !== viewLocation) { - this.moveViews([viewDescriptor], viewLocation, defaultLocation); - } - } - } - })); - } - - private get viewsCanMoveSettingValue(): boolean { - return !!this.configurationService.getValue('workbench.view.experimental.allowMovingToNewContainer'); } private registerGroupedViews(groupedViews: Map): void { @@ -377,7 +352,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } private shouldGenerateContainer(containerInfo: ICachedViewContainerInfo): boolean { - return !!containerInfo.sourceViewId && containerInfo.location !== undefined && (this.viewsCanMoveSettingValue || containerInfo.sourceViewId === SEARCH_VIEW_ID); + return !!containerInfo.sourceViewId && containerInfo.location !== undefined; } private onDidDeregisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { @@ -445,11 +420,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { - let container = this.getDefaultContainer(view.id)!; - if (this.getViewContainerLocation(container) !== location) { - container = this.registerViewContainerForSingleView(view, location); - } - + let container = this.registerViewContainerForSingleView(view, location); this.moveViewsToContainer([view], container); } @@ -491,7 +462,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor id, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true }]), name: sourceView.name, - icon: sourceView.containerIcon, + icon: location === ViewContainerLocation.Sidebar ? sourceView.containerIcon || 'codicon-window' : undefined, hideIfEmpty: true }, location); } @@ -675,12 +646,17 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const sourceViewId = this.generatedContainerSourceViewIds.get(container.id); views.forEach(view => { this.cachedViewInfo.set(view.id, { containerId: container.id, location, sourceViewId }); + this.getOrCreateDefaultViewLocationContextKey(view).set(this.getDefaultContainer(view.id) === container); }); this.getViewDescriptors(container).addViews(views); } private removeViews(container: ViewContainer, views: IViewDescriptor[]): void { + // Set view default location keys to false + views.forEach(view => this.getOrCreateDefaultViewLocationContextKey(view).set(false)); + + // Remove the views this.getViewDescriptors(container).removeViews(views); } @@ -703,6 +679,16 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } return contextKey; } + + private getOrCreateDefaultViewLocationContextKey(viewDescriptor: IViewDescriptor): IContextKey { + const defaultViewLocationContextKeyId = `${viewDescriptor.id}.defaultViewLocation`; + let contextKey = this.defaultViewLocationContextKeys.get(defaultViewLocationContextKeyId); + if (!contextKey) { + contextKey = new RawContextKey(defaultViewLocationContextKeyId, false).bindTo(this.contextKeyService); + this.defaultViewLocationContextKeys.set(defaultViewLocationContextKeyId, contextKey); + } + return contextKey; + } } registerSingleton(IViewDescriptorService, ViewDescriptorService); diff --git a/src/vs/workbench/test/browser/parts/views/views.test.ts b/src/vs/workbench/test/browser/parts/views/views.test.ts index ed6066c568..cc82415cc0 100644 --- a/src/vs/workbench/test/browser/parts/views/views.test.ts +++ b/src/vs/workbench/test/browser/parts/views/views.test.ts @@ -535,8 +535,8 @@ suite('ViewDescriptorService', () => { sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - assert.equal(sidebarViews.activeViewDescriptors.length, 2, 'Sidebar should have 2 views'); - assert.equal(panelViews.activeViewDescriptors.length, 1, 'Panel should have 1 view'); + assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); + assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel should have 1 view'); assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[0].id), ViewContainerLocation.Sidebar, 'View should be located in the sidebar'); assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[2].id), ViewContainerLocation.Panel, 'View should be located in the panel'); @@ -602,11 +602,11 @@ suite('ViewDescriptorService', () => { expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); expectedSequence += containerMoveString(viewDescriptors[0], viewDescriptorService.getViewContainer(viewDescriptors[0].id)!, sidebarContainer); - viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Sidebar); + viewDescriptorService.moveViewsToContainer([viewDescriptors[0]], sidebarContainer); expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); expectedSequence += containerMoveString(viewDescriptors[2], viewDescriptorService.getViewContainer(viewDescriptors[2].id)!, panelContainer); - viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Panel); + viewDescriptorService.moveViewsToContainer([viewDescriptors[2]], panelContainer); expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); expectedSequence += containerMoveString(viewDescriptors[0], sidebarContainer, panelContainer); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index d9159fde73..165846f44d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -105,6 +105,7 @@ import { IListService } from 'vs/platform/list/browser/listService'; import { win32, posix } from 'vs/base/common/path'; import { TestWorkingCopyService, TestContextService, TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; import { IViewsService, IView } from 'vs/workbench/common/views'; +import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -168,6 +169,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IViewletService, new TestViewletService()); instantiationService.stub(IListService, new TestListService()); instantiationService.stub(IQuickInputService, new QuickInputService(TestEnvironmentService, configService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService)); + instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService()); return instantiationService; } diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 0eacc56989..a83788d32d 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -49,6 +49,7 @@ import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; +import 'vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 4117050ebd..568a57d9b2 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -63,6 +63,7 @@ import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { StorageKeysSyncRegistryService, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; @@ -84,6 +85,7 @@ registerSingleton(IAuthenticationService, AuthenticationService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); +registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); registerSingleton(IAuthenticationTokenService, AuthenticationTokenService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(IUserDataSyncService, UserDataSyncService); diff --git a/test/automation/src/quickinput.ts b/test/automation/src/quickinput.ts index 7f428516fb..6db42b6f43 100644 --- a/test/automation/src/quickinput.ts +++ b/test/automation/src/quickinput.ts @@ -9,10 +9,19 @@ export class QuickInput { static QUICK_INPUT = '.quick-input-widget'; static QUICK_INPUT_INPUT = `${QuickInput.QUICK_INPUT} .quick-input-box input`; - static QUICK_INPUT_FOCUSED_ELEMENT = `${QuickInput.QUICK_INPUT} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`; + static QUICK_INPUT_ROW = `${QuickInput.QUICK_INPUT} .quick-input-list .monaco-list-row`; + static QUICK_INPUT_FOCUSED_ELEMENT = `${QuickInput.QUICK_INPUT_ROW}.focused .monaco-highlighted-label`; + static QUICK_INPUT_ENTRY_LABEL = `${QuickInput.QUICK_INPUT_ROW} .label-name`; + static QUICK_INPUT_ENTRY_LABEL_SPAN = `${QuickInput.QUICK_INPUT_ROW} .monaco-highlighted-label span`; constructor(private code: Code) { } + async submit(text: string): Promise { + await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, text); + await this.code.dispatchKeybinding('enter'); + await this.waitForQuickInputClosed(); + } + async closeQuickInput(): Promise { await this.code.dispatchKeybinding('escape'); await this.waitForQuickInputClosed(); @@ -22,7 +31,11 @@ export class QuickInput { await this.code.waitForActiveElement(QuickInput.QUICK_INPUT_INPUT, retryCount); } - private async waitForQuickInputClosed(): Promise { + async waitForQuickInputElements(accept: (names: string[]) => boolean): Promise { + await this.code.waitForElements(QuickInput.QUICK_INPUT_ENTRY_LABEL, false, els => accept(els.map(e => e.textContent))); + } + + async waitForQuickInputClosed(): Promise { await this.code.waitForElement(QuickInput.QUICK_INPUT, r => !!r && r.attributes.style.indexOf('display: none;') !== -1); } diff --git a/test/automation/src/quickopen.ts b/test/automation/src/quickopen.ts index fa8687b009..baf5777735 100644 --- a/test/automation/src/quickopen.ts +++ b/test/automation/src/quickopen.ts @@ -5,17 +5,11 @@ import { Editors } from './editors'; import { Code } from './code'; +import { QuickInput } from './quickinput'; export class QuickOpen { - static QUICK_OPEN = 'div.monaco-quick-open-widget'; - static QUICK_OPEN_HIDDEN = 'div.monaco-quick-open-widget[aria-hidden="true"]'; - static QUICK_OPEN_INPUT = `${QuickOpen.QUICK_OPEN} .quick-open-input input`; - static QUICK_OPEN_FOCUSED_ELEMENT = `${QuickOpen.QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`; - static QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry'; - static QUICK_OPEN_ENTRY_LABEL_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry .label-name'; - - constructor(private code: Code, private editors: Editors) { } + constructor(private code: Code, private editors: Editors, private quickInput: QuickInput) { } async openQuickOpen(value: string): Promise { let retries = 0; @@ -29,7 +23,7 @@ export class QuickOpen { } try { - await this.waitForQuickOpenOpened(10); + await this.quickInput.waitForQuickInputOpened(10); break; } catch (err) { if (++retries > 5) { @@ -41,59 +35,27 @@ export class QuickOpen { } if (value) { - await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, value); + await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, value); } } - async closeQuickOpen(): Promise { - await this.code.dispatchKeybinding('escape'); - await this.waitForQuickOpenClosed(); - } - async openFile(fileName: string): Promise { await this.openQuickOpen(fileName); - await this.waitForQuickOpenElements(names => names[0] === fileName); + await this.quickInput.waitForQuickInputElements(names => names[0] === fileName); await this.code.dispatchKeybinding('enter'); await this.editors.waitForActiveTab(fileName); await this.editors.waitForEditorFocus(fileName); } - async waitForQuickOpenOpened(retryCount?: number): Promise { - await this.code.waitForActiveElement(QuickOpen.QUICK_OPEN_INPUT, retryCount); - } - - private async waitForQuickOpenClosed(): Promise { - await this.code.waitForElement(QuickOpen.QUICK_OPEN_HIDDEN); - } - - async submit(text: string): Promise { - await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, text); - await this.code.dispatchKeybinding('enter'); - await this.waitForQuickOpenClosed(); - } - - async selectQuickOpenElement(index: number): Promise { - await this.waitForQuickOpenOpened(); - for (let from = 0; from < index; from++) { - await this.code.dispatchKeybinding('down'); - } - await this.code.dispatchKeybinding('enter'); - await this.waitForQuickOpenClosed(); - } - - async waitForQuickOpenElements(accept: (names: string[]) => boolean): Promise { - await this.code.waitForElements(QuickOpen.QUICK_OPEN_ENTRY_LABEL_SELECTOR, false, els => accept(els.map(e => e.textContent))); - } - - async runCommand(command: string): Promise { - await this.openQuickOpen(`> ${command}`); + async runCommand(commandId: string): Promise { + await this.openQuickOpen(`>${commandId}`); // wait for best choice to be focused - await this.code.waitForTextContent(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT, command); + await this.code.waitForTextContent(QuickInput.QUICK_INPUT_FOCUSED_ELEMENT); // wait and click on best choice - await this.code.waitAndClick(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT); + await this.quickInput.selectQuickInputElement(0); } async openQuickOutline(): Promise { @@ -106,13 +68,13 @@ export class QuickOpen { await this.code.dispatchKeybinding('ctrl+shift+o'); } - const text = await this.code.waitForTextContent('div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties div.monaco-tree-row .quick-open-entry .monaco-icon-label .label-name .monaco-highlighted-label span'); + const text = await this.code.waitForTextContent(QuickInput.QUICK_INPUT_ENTRY_LABEL_SPAN); if (text !== 'No symbol information for the file') { return; } - await this.closeQuickOpen(); + await this.quickInput.closeQuickInput(); await new Promise(c => setTimeout(c, 250)); } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index e92dfe8a25..561469132d 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -32,6 +32,6 @@ export class SettingsEditor { } private async openSettings(): Promise { - await this.quickopen.runCommand('Preferences: Open Settings (JSON)'); + await this.quickopen.runCommand('workbench.action.openSettingsJson'); } } diff --git a/test/automation/src/sql/queryEditors.ts b/test/automation/src/sql/queryEditors.ts index d0b09ae88e..74c423de64 100644 --- a/test/automation/src/sql/queryEditors.ts +++ b/test/automation/src/sql/queryEditors.ts @@ -4,32 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { Editors } from '../editors'; -import { QuickOpen } from '../quickopen'; import { Code } from '../code'; -import * as path from 'path'; export class QueryEditors extends Editors { constructor( - private vsCode: Code, - private quickopen: QuickOpen + private vsCode: Code ) { super(vsCode); } - /** - * Opens the specified file - this correctly handles SQL files which are opened in a Query Editor window - * @param filePath The full path of the file to open. - */ - async openFile(filePath: string): Promise { - await this.quickopen.openQuickOpen(filePath); - - const fileBaseName = path.basename(filePath); - await this.quickopen.waitForQuickOpenElements(names => names[0] === fileBaseName); - await this.vsCode.dispatchKeybinding('enter'); - await this.waitForEditorFocus(fileBaseName); - } - /** * Waits for an active SQL Query Editor tab for the specified file. This is a modification of the editors.waitForActiveTab that * takes into account the connected status displayed in the title of Query Editors. @@ -41,7 +25,6 @@ export class QueryEditors extends Editors { await this.vsCode.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][aria-label="${fileName} - disconnected, tab"]`); } - /** * Waits for an active Query Editor for the specified file to have focus. This is a modification of the editors.waitForEditorFocus * that takes into account the connected status displayed in the title of Query Editors. diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 4016d9e455..7306df4d6b 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -15,7 +15,7 @@ export class Terminal { constructor(private code: Code, private quickopen: QuickOpen) { } async showTerminal(): Promise { - await this.quickopen.runCommand('View: Toggle Integrated Terminal'); + await this.quickopen.runCommand('workbench.action.terminal.toggleTerminal'); await this.code.waitForActiveElement(XTERM_TEXTAREA); await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0)); } diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index 447351b4a1..654abdae3f 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -56,8 +56,8 @@ export class Workbench { constructor(code: Code, userDataPath: string) { this.editors = new Editors(code); - this.quickopen = new QuickOpen(code, this.editors); this.quickinput = new QuickInput(code); + this.quickopen = new QuickOpen(code, this.editors, this.quickinput); this.explorer = new Explorer(code, this.editors); this.activitybar = new ActivityBar(code); this.search = new Search(code); @@ -73,7 +73,7 @@ export class Workbench { // {{SQL CARBON EDIT}} this.connectionDialog = new ConnectionDialog(code); this.profiler = new Profiler(code, this.quickopen); - this.queryEditors = new QueryEditors(code, this.quickopen); + this.queryEditors = new QueryEditors(code); // {{END}} } } diff --git a/test/smoke/src/areas/editor/editor.test.ts b/test/smoke/src/areas/editor/editor.test.ts index 27884dd73e..fdfce67738 100644 --- a/test/smoke/src/areas/editor/editor.test.ts +++ b/test/smoke/src/areas/editor/editor.test.ts @@ -12,7 +12,7 @@ export function setup() { await app.workbench.quickopen.openFile('www'); await app.workbench.quickopen.openQuickOutline(); - await app.workbench.quickopen.waitForQuickOpenElements(names => names.length >= 6); + await app.workbench.quickinput.waitForQuickInputElements(names => names.length >= 6); }); // it('folds/unfolds the code correctly', async function () { diff --git a/test/smoke/src/areas/languages/languages.test.ts b/test/smoke/src/areas/languages/languages.test.ts index ef75ef61c5..3edac049cc 100644 --- a/test/smoke/src/areas/languages/languages.test.ts +++ b/test/smoke/src/areas/languages/languages.test.ts @@ -12,7 +12,7 @@ export function setup() { await app.workbench.quickopen.openFile('style.css'); await app.workbench.quickopen.openQuickOutline(); - await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 2); + await app.workbench.quickinput.waitForQuickInputElements(names => names.length === 2); }); it('verifies problems view', async function () { diff --git a/test/smoke/src/areas/multiroot/multiroot.test.ts b/test/smoke/src/areas/multiroot/multiroot.test.ts index 1f024d2f9c..54f4b92ae8 100644 --- a/test/smoke/src/areas/multiroot/multiroot.test.ts +++ b/test/smoke/src/areas/multiroot/multiroot.test.ts @@ -47,8 +47,8 @@ export function setup() { const app = this.app as Application; await app.workbench.quickopen.openQuickOpen('*.*'); - await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 6); - await app.workbench.quickopen.closeQuickOpen(); + await app.workbench.quickinput.waitForQuickInputElements(names => names.length === 6); + await app.workbench.quickinput.closeQuickInput(); }); it('shows workspace name in title', async function () { diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 8703d7e4d3..e20f8ac9c9 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -10,8 +10,8 @@ export function setup() { describe('Search', () => { after(function () { const app = this.app as Application; - cp.execSync('git checkout .', { cwd: app.workspacePathOrFolder }); - cp.execSync('git reset --hard origin/master', { cwd: app.workspacePathOrFolder }); + cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder }); + cp.execSync('git reset --hard origin/master --quiet', { cwd: app.workspacePathOrFolder }); }); it('searches for body & checks for correct result number', async function () { @@ -71,7 +71,7 @@ export function setup() { ]; await app.workbench.quickopen.openQuickOpen('.js'); - await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); + await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(n => names.some(m => n === m))); await app.code.dispatchKeybinding('escape'); }); @@ -84,7 +84,7 @@ export function setup() { ]; await app.workbench.quickopen.openQuickOpen('a.s'); - await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); + await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(n => names.some(m => n === m))); await app.code.dispatchKeybinding('escape'); }); }); diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index 3e94518631..a8d20bc21f 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -77,9 +77,9 @@ export function setup(isWeb) { await app.workbench.quickopen.openFile('app.js'); await app.workbench.statusbar.clickOn(StatusBarElement.SELECTION_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); + await app.workbench.quickinput.waitForQuickInputOpened(); - await app.workbench.quickopen.submit(':15'); + await app.workbench.quickinput.submit(':15'); await app.workbench.editor.waitForHighlightingLine('app.js', 15); }); diff --git a/test/smoke/src/sql/queryEditor/queryEditor.test.ts b/test/smoke/src/sql/queryEditor/queryEditor.test.ts index a269a52936..18e2ef0f14 100644 --- a/test/smoke/src/sql/queryEditor/queryEditor.test.ts +++ b/test/smoke/src/sql/queryEditor/queryEditor.test.ts @@ -16,7 +16,7 @@ export function setup() { await fs.writeFile(testFilePath, ''); try { const app = this.app as Application; - await app.workbench.queryEditors.openFile(testFilePath); + await app.workbench.quickopen.openFile(testFilePath); const fileBaseName = path.basename(testFilePath); await app.workbench.editor.waitForTypeInEditor(fileBaseName, 'SELECT * FROM sys.tables'); } diff --git a/test/unit/README.md b/test/unit/README.md index f471ebb7d8..cf338465a7 100644 --- a/test/unit/README.md +++ b/test/unit/README.md @@ -4,7 +4,7 @@ ./scripts/test.[sh|bat] -All unit tests are run inside a electron-browser environment which access to DOM and Nodejs api. This is the closest to the enviroment in which VS Code itself ships. Notes: +All unit tests are run inside a electron-browser environment which access to DOM and Nodejs api. This is the closest to the environment in which VS Code itself ships. Notes: - use the `--debug` to see an electron window with dev tools which allows for debugging - to run only a subset of tests use the `--run` or `--glob` options