diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index ddc65ea119f..bd9188f8ee1 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.2.2517", + "version": "2020.2.2617", "forQualities": [ "insider" ], diff --git a/extensions/git/package.json b/extensions/git/package.json index 1eaf39ad44b..5246020c6c2 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1825,37 +1825,37 @@ }, "viewsWelcome": [ { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.disabled%", "when": "!config.git.enabled" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.missing%", "when": "config.git.enabled && git.missing" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.empty%", "when": "config.git.enabled && !git.missing && workbenchState == empty" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.folder%", "when": "config.git.enabled && !git.missing && workbenchState == folder" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.workspace%", "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount != 0" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.emptyWorkspace%", "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0" }, { - "view": "workbench.explorer.emptyView", + "view": "explorer", "contents": "%view.workbench.cloneRepository%" } ] diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6328edd99bf..e163ffad48d 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -150,11 +150,11 @@ "colors.ignored": "Color for ignored resources.", "colors.conflict": "Color for resources with conflicts.", "colors.submodule": "Color for submodule resources.", - "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use Git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", - "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.emptyWorkspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.cloneRepository": "You can also clone a repository from a URL. To learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).\n[Clone Repository](command:git.clone)" + "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", + "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.emptyWorkspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.cloneRepository": "You can also clone a repository from a URL. To learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).\n[Clone Repository](command:git.clone)" } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index ad4d47485e7..a27fa783fc0 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1397,12 +1397,16 @@ export class CommandCenter { opts.signoff = true; } + const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges'); + if ( ( // no changes (noStagedChanges && noUnstagedChanges) // or no staged changes and not `all` || (!opts.all && noStagedChanges) + // no staged changes and no tracked unstaged changes + || (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED)) ) && !opts.empty ) { @@ -1416,7 +1420,7 @@ export class CommandCenter { return false; } - if (opts.all && config.get<'all' | 'tracked'>('smartCommitChanges') === 'tracked') { + if (opts.all && smartCommitChanges === 'tracked') { opts.all = 'tracked'; } @@ -2353,7 +2357,7 @@ export class CommandCenter { else if (item.previousRef === 'HEAD' && item.ref === '~') { title = localize('git.title.index', '{0} (Index)', basename); } else { - title = localize('git.title.diffRefs', '{0} ({1}) \u27f7 {0} ({2})', basename, item.shortPreviousRef, item.shortRef); + title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef); } return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title); diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 943db6f20b5..c377e020939 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -16,7 +16,7 @@ export async function activate(context: vscode.ExtensionContext) { await loginService.initialize(); vscode.authentication.registerAuthenticationProvider({ - id: 'GitHub', + id: 'github', displayName: 'GitHub', onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 0f46d20d81b..52c7c4e333a 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -71,7 +71,7 @@ export class GitHubAuthenticationProvider { id: session.id, accountName: session.accountName, scopes: session.scopes, - accessToken: () => Promise.resolve(session.accessToken) + getAccessToken: () => Promise.resolve(session.accessToken) }; }); } catch (e) { @@ -84,7 +84,7 @@ export class GitHubAuthenticationProvider { private async storeSessions(): Promise { const sessionData: SessionData[] = await Promise.all(this._sessions.map(async session => { - const resolvedAccessToken = await session.accessToken(); + const resolvedAccessToken = await session.getAccessToken(); return { id: session.id, accountName: session.accountName, @@ -111,7 +111,7 @@ export class GitHubAuthenticationProvider { const userInfo = await this._githubServer.getUserInfo(token); return { id: userInfo.id, - accessToken: () => Promise.resolve(token), + getAccessToken: () => Promise.resolve(token), accountName: userInfo.accountName, scopes: scopes }; diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index cad4e5196b9..4a625231458 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -7,9 +7,10 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; +import { Disposable } from '../utils/dispose'; +import * as fileSchemes from '../utils/fileSchemes'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { ResourceMap } from '../utils/resourceMap'; -import { Disposable } from '../utils/dispose'; function objsAreEqual(a: T, b: T): boolean { @@ -144,9 +145,7 @@ export default class FileConfigurationManager extends Disposable { isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format', document.uri); - // `semicolons` added to `Proto.FormatCodeSettings` in TypeScript 3.7: - // remove intersection type after upgrading TypeScript. - const settings: Proto.FormatCodeSettings & { semicolons?: string } = { + return { tabSize: options.tabSize, indentSize: options.tabSize, convertTabsToSpaces: options.insertSpaces, @@ -169,8 +168,6 @@ export default class FileConfigurationManager extends Disposable { placeOpenBraceOnNewLineForControlBlocks: config.get('placeOpenBraceOnNewLineForControlBlocks'), semicolons: config.get('semicolons'), }; - - return settings; } private getPreferences(document: vscode.TextDocument): Proto.UserPreferences { @@ -185,7 +182,7 @@ export default class FileConfigurationManager extends Disposable { return { quotePreference: this.getQuoteStylePreference(config), importModuleSpecifierPreference: getImportModuleSpecifierPreference(config), - allowTextChangesInNewFiles: document.uri.scheme === 'file', + allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file, providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true), allowRenameOfImportPath: true, }; diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 5f193828141..16e7990be0b 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -184,7 +184,7 @@ export class AzureActiveDirectoryService { private convertToSession(token: IToken): vscode.AuthenticationSession { return { id: token.sessionId, - accessToken: () => this.resolveAccessToken(token), + getAccessToken: () => this.resolveAccessToken(token), accountName: token.accountName, scopes: token.scope.split(' ') }; @@ -192,7 +192,9 @@ export class AzureActiveDirectoryService { private async resolveAccessToken(token: IToken): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { - Logger.info('Token available from cache'); + token.expiresAt + ? Logger.info(`Token available from cache, expires in ${token.expiresAt - Date.now()} milliseconds`) + : Logger.info('Token available from cache'); return Promise.resolve(token.accessToken); } diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index fa94280fe3d..e70fbe59cb0 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -18,7 +18,7 @@ export async function activate(context: vscode.ExtensionContext) { await loginService.initialize(); context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ - id: 'MSA', + id: 'microsoft', displayName: 'Microsoft', onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index 2a1c4395187..9f4eb6a23b8 100755 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -2,7 +2,7 @@ # On Fedora $SNAP is under /var and there is some magic to map it to /snap. # We need to handle that case and reset $SNAP -SNAP=$(echo $SNAP | sed -e "s|/var/lib/snapd||g") +SNAP=$(echo "$SNAP" | sed -e "s|/var/lib/snapd||g") if [ "$SNAP_ARCH" == "amd64" ]; then ARCH="x86_64-linux-gnu" @@ -14,21 +14,21 @@ else ARCH="$SNAP_ARCH-linux-gnu" fi -export XDG_CACHE_HOME=$SNAP_USER_COMMON/.cache -if [[ -d $SNAP_USER_DATA/.cache && ! -e $XDG_CACHE_HOME ]]; then +GDK_CACHE_DIR="$SNAP_USER_COMMON/.cache" +if [[ -d "$SNAP_USER_DATA/.cache" && ! -e "$GDK_CACHE_DIR" ]]; then # the .cache directory used to be stored under $SNAP_USER_DATA, migrate it - mv $SNAP_USER_DATA/.cache $SNAP_USER_COMMON/ + mv "$SNAP_USER_DATA/.cache" "$SNAP_USER_COMMON/" fi -mkdir -p $XDG_CACHE_HOME +[ ! -d "$GDK_CACHE_DIR" ] && mkdir -p "$GDK_CACHE_DIR" # Gdk-pixbuf loaders -export GDK_PIXBUF_MODULE_FILE=$XDG_CACHE_HOME/gdk-pixbuf-loaders.cache -export GDK_PIXBUF_MODULEDIR=$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders -if [ -f $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders ]; then - $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders > $GDK_PIXBUF_MODULE_FILE +export GDK_PIXBUF_MODULE_FILE="$GDK_CACHE_DIR/gdk-pixbuf-loaders.cache" +export GDK_PIXBUF_MODULEDIR="$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders" +if [ -f "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" ]; then + "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" > "$GDK_PIXBUF_MODULE_FILE" fi # Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed) -[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p $XDG_RUNTIME_DIR -m 700 +[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p "$XDG_RUNTIME_DIR" -m 700 exec "$@" diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 41efce5c63d..b71ef2dafa5 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?279add2ec8b3d516ca20a123230cbf9f") format("truetype"); + src: url("./codicon.ttf?b5dd8f5aa953889dc1f4c9fa9b44d3dd") format("truetype"); } .codicon[class*='codicon-'] { @@ -415,5 +415,6 @@ .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } .codicon-bell-dot:before { content: "\f101" } -.codicon-debug-alt-2:before { content: "\f102" } -.codicon-debug-alt:before { content: "\f103" } +.codicon-bell-progress:before { content: "\f102" } +.codicon-debug-alt-2:before { content: "\f103" } +.codicon-debug-alt:before { content: "\f104" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index df86f7d4d9a..845205f5b3a 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/common/linkedText.ts b/src/vs/base/common/linkedText.ts index ee268d9fdcd..5f6c6a05dcf 100644 --- a/src/vs/base/common/linkedText.ts +++ b/src/vs/base/common/linkedText.ts @@ -23,7 +23,7 @@ export class LinkedText { } } -const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; +const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: ("|')([^\3]+)(\3))?\)/gi; export function parseLinkedText(text: string): LinkedText { const result: LinkedTextNode[] = []; @@ -36,7 +36,7 @@ export function parseLinkedText(text: string): LinkedText { result.push(text.substring(index, match.index)); } - const [, label, href, title] = match; + const [, label, href, , title] = match; if (title) { result.push({ label, href, title }); diff --git a/src/vs/base/parts/composite/browser/compositeDnd.ts b/src/vs/base/parts/composite/browser/compositeDnd.ts index ca8b52d491a..6091c8a10d3 100644 --- a/src/vs/base/parts/composite/browser/compositeDnd.ts +++ b/src/vs/base/parts/composite/browser/compositeDnd.ts @@ -21,4 +21,5 @@ export class CompositeDragAndDropData implements IDragAndDropData { export interface ICompositeDragAndDrop { drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): void; onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; + onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; } diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 059eb29406d..d3d94acb6ad 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -397,7 +397,7 @@ class QuickPick extends QuickInput implements IQuickPi private _valueSelection: Readonly<[number, number]> | undefined; private valueSelectionUpdated = true; private _validationMessage: string | undefined; - private _ok = false; + private _ok: boolean | 'default' = 'default'; private _customButton = false; private _customButtonLabel: string | undefined; private _customButtonHover: string | undefined; @@ -566,7 +566,7 @@ class QuickPick extends QuickInput implements IQuickPi return this._ok; } - set ok(showOkButton: boolean) { + set ok(showOkButton: boolean | 'default') { this._ok = showOkButton; this.update(); } @@ -757,7 +757,8 @@ class QuickPick extends QuickInput implements IQuickPi if (!this.visible) { return; } - this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: this.ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok: this.ok }); + const ok = this.ok === 'default' ? this.canSelectMany : this.ok; + this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 36905c59c53..10c8255a444 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -162,7 +162,7 @@ export interface IQuickPick extends IQuickInput { readonly onDidAccept: Event; - ok: boolean; + ok: boolean | 'default'; readonly onDidCustom: Event; diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index 03dedeca57f..2c2c909a9d1 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -27,7 +27,8 @@ export interface IUpdateRequest { } export interface IStorageItemsChangeEvent { - items: Map; + changed?: Map; + deleted?: Set; } export interface IStorageDatabase { @@ -104,10 +105,11 @@ export class Storage extends Disposable implements IStorage { // items that change external require us to update our // caches with the values. we just accept the value and // emit an event if there is a change. - e.items.forEach((value, key) => this.accept(key, value)); + e.changed?.forEach((value, key) => this.accept(key, value)); + e.deleted?.forEach(key => this.accept(key, undefined)); } - private accept(key: string, value: string): void { + private accept(key: string, value: string | undefined): void { if (this.state === StorageState.Closed) { return; // Return early if we are already closed } @@ -315,4 +317,4 @@ export class InMemoryStorageDatabase implements IStorageDatabase { close(): Promise { return Promise.resolve(); } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 9c966c0868b..5c25b92dffc 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -124,28 +124,27 @@ suite('Storage Library', () => { changes.clear(); // Nothing happens if changing to same value - const change = new Map(); - change.set('foo', 'bar'); - database.fireDidChangeItemsExternal({ items: change }); + const changed = new Map(); + changed.set('foo', 'bar'); + database.fireDidChangeItemsExternal({ changed }); equal(changes.size, 0); // Change is accepted if valid - change.set('foo', 'bar1'); - database.fireDidChangeItemsExternal({ items: change }); + changed.set('foo', 'bar1'); + database.fireDidChangeItemsExternal({ changed }); ok(changes.has('foo')); equal(storage.get('foo'), 'bar1'); changes.clear(); // Delete is accepted - change.set('foo', undefined!); - database.fireDidChangeItemsExternal({ items: change }); + const deleted = new Set(['foo']); + database.fireDidChangeItemsExternal({ deleted }); ok(changes.has('foo')); - equal(storage.get('foo', null!), null); + equal(storage.get('foo', undefined), undefined); changes.clear(); // Nothing happens if changing to same value - change.set('foo', undefined!); - database.fireDidChangeItemsExternal({ items: change }); + database.fireDidChangeItemsExternal({ deleted }); equal(changes.size, 0); await storage.close(); diff --git a/src/vs/base/test/common/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts index 8e1cb887485..a7b61a558c2 100644 --- a/src/vs/base/test/common/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -21,6 +21,21 @@ suite('LinkedText', () => { { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a title\').').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a title' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a \'title\'").').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a \'title\'' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a "title"\').').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a "title"' }, + '.' + ]); assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ 'Some message with [link text](random stuff).' ]); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 09ccca9b823..9684cbd4c20 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -75,7 +75,11 @@ function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, in // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => getProcessItem(processes, child, indent + 1, isLocal)); + item.children.forEach(child => { + if (child) { + getProcessItem(processes, child, indent + 1, isLocal); + } + }); } } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 1f1c08a58f1..55600018311 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -32,6 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; const RUN_TEXTMATE_IN_WORKER = false; @@ -100,7 +101,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { super(); @@ -244,6 +246,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { get isExtensionTestHost(): boolean { return !!(this.config && this.config.extensionTestsPath); } + get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this.config?.debugId; } + setRepresentedFilename(filename: string): void { if (isMacintosh) { this.win.setRepresentedFilename(filename); @@ -447,6 +451,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { private onWindowError(error: WindowError): void { this.logService.error(error === WindowError.CRASHED ? '[VS Code]: render process crashed!' : '[VS Code]: detected unresponsive'); + // If we run extension tests from CLI, showing a dialog is not + // very helpful in this case. Rather, we bring down the test run + // to signal back a failing run. + if (this.isExtensionDevelopmentTestFromCli) { + this.lifecycleMainService.kill(1); + return; + } + + // Telemetry type WindowErrorClassification = { type: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; }; diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 3d18020da6f..f6fdac9c874 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -16,6 +16,7 @@ export interface IBulkEditOptions { progress?: IProgress; showPreview?: boolean; label?: string; + quotableLabel?: string; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index d5aa95d0611..ed05d886991 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -46,7 +46,7 @@ class MinimapOptions { public readonly renderMinimap: RenderMinimap; - public readonly mode: 'actual' | 'cover' | 'contain'; + public readonly size: 'proportional' | 'fill' | 'fit'; public readonly minimapHeightIsEditorHeight: boolean; @@ -108,7 +108,7 @@ class MinimapOptions { const minimapOpts = options.get(EditorOption.minimap); this.renderMinimap = layoutInfo.renderMinimap | 0; - this.mode = minimapOpts.mode; + this.size = minimapOpts.size; this.minimapHeightIsEditorHeight = layoutInfo.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); this.showSlider = minimapOpts.showSlider; @@ -144,7 +144,7 @@ class MinimapOptions { public equals(other: MinimapOptions): boolean { return (this.renderMinimap === other.renderMinimap - && this.mode === other.mode + && this.size === other.size && this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight && this.scrollBeyondLastLine === other.scrollBeyondLastLine && this.showSlider === other.showSlider @@ -1111,7 +1111,7 @@ class InnerMinimap extends Disposable { if (!this._lastRenderData) { return; } - if (this._model.options.minimapHeightIsEditorHeight) { + if (this._model.options.size !== 'proportional') { if (e.leftButton && this._lastRenderData) { // pretend the click occured in the center of the slider const position = dom.getDomNodePagePosition(this._slider.domNode); diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index cfa24de682f..2da217bb864 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -617,10 +617,11 @@ export class DiffReview extends Disposable { header.setAttribute('role', 'listitem'); container.appendChild(header); + const lineHeight = modifiedOptions.get(EditorOption.lineHeight); let modLine = minModifiedLine; for (let i = 0, len = diffs.length; i < len; i++) { const diffEntry = diffs[i]; - DiffReview._renderSection(container, diffEntry, modLine, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts); + DiffReview._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts); if (diffEntry.modifiedLineStart !== 0) { modLine = diffEntry.modifiedLineEnd; } @@ -632,7 +633,7 @@ export class DiffReview extends Disposable { } private static _renderSection( - dest: HTMLElement, diffEntry: DiffEntry, modLine: number, width: number, + dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number, originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions ): void { @@ -641,17 +642,18 @@ export class DiffReview extends Disposable { let rowClassName: string = 'diff-review-row'; let lineNumbersExtraClassName: string = ''; - let spacerClassName: string = 'diff-review-spacer'; + const spacerClassName: string = 'diff-review-spacer'; + let spacerCodiconName: string | null = null; switch (type) { case DiffEntryType.Insert: rowClassName = 'diff-review-row line-insert'; lineNumbersExtraClassName = ' char-insert'; - spacerClassName = 'diff-review-spacer insert-sign'; + spacerCodiconName = 'codicon codicon-add'; break; case DiffEntryType.Delete: rowClassName = 'diff-review-row line-delete'; lineNumbersExtraClassName = ' char-delete'; - spacerClassName = 'diff-review-spacer delete-sign'; + spacerCodiconName = 'codicon codicon-remove'; break; } @@ -686,6 +688,7 @@ export class DiffReview extends Disposable { let cell = document.createElement('div'); cell.className = 'diff-review-cell'; + cell.style.height = `${lineHeight}px`; row.appendChild(cell); const originalLineNumber = document.createElement('span'); @@ -713,7 +716,15 @@ export class DiffReview extends Disposable { const spacer = document.createElement('span'); spacer.className = spacerClassName; - spacer.innerHTML = '  '; + + if (spacerCodiconName) { + const spacerCodicon = document.createElement('span'); + spacerCodicon.className = spacerCodiconName; + spacerCodicon.innerHTML = '  '; + spacer.appendChild(spacerCodicon); + } else { + spacer.innerHTML = '  '; + } cell.appendChild(spacer); let lineContent: string; diff --git a/src/vs/editor/browser/widget/media/diffReview.css b/src/vs/editor/browser/widget/media/diffReview.css index b2b17028489..56c6f15cbf6 100644 --- a/src/vs/editor/browser/widget/media/diffReview.css +++ b/src/vs/editor/browser/widget/media/diffReview.css @@ -37,13 +37,14 @@ width: 100%; } -.monaco-diff-editor .diff-review-cell { - display: table-cell; -} - .monaco-diff-editor .diff-review-spacer { display: inline-block; width: 10px; + vertical-align: middle; +} + +.monaco-diff-editor .diff-review-spacer > .codicon { + font-size: 9px !important; } .monaco-diff-editor .diff-review-actions { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 270e0018dff..284c6f904bb 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1802,7 +1802,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 2 ? Math.round(minimap.scale * 2) : minimap.scale); const minimapMaxColumn = minimap.maxColumn | 0; - const minimapMode = minimap.mode; + const minimapSize = minimap.size; const scrollbar = options.get(EditorOption.scrollbar); const verticalScrollbarWidth = scrollbar.verticalScrollbarSize | 0; @@ -1866,7 +1866,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption minimapCanvasInnerHeight) { + if (minimapSize === 'fill' || effectiveMinimapHeight > minimapCanvasInnerHeight) { minimapHeightIsEditorHeight = true; const configuredFontScale = minimapScale; minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); @@ -2074,7 +2074,7 @@ export interface IEditorMinimapOptions { * Control the minimap rendering mode. * Defaults to 'actual'. */ - mode?: 'actual' | 'cover' | 'contain'; + size?: 'proportional' | 'fill' | 'fit'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. @@ -2103,7 +2103,7 @@ class EditorMinimap extends BaseEditorOption(input.mode, this.defaultValue.mode, ['actual', 'cover', 'contain']), + size: EditorStringEnumOption.stringSet<'proportional' | 'fill' | 'fit'>(input.size, this.defaultValue.size, ['proportional', 'fill', 'fit']), side: EditorStringEnumOption.stringSet<'right' | 'left'>(input.side, this.defaultValue.side, ['right', 'left']), showSlider: EditorStringEnumOption.stringSet<'always' | 'mouseover'>(input.showSlider, this.defaultValue.showSlider, ['always', 'mouseover']), renderCharacters: EditorBooleanOption.boolean(input.renderCharacters, this.defaultValue.renderCharacters), @@ -2834,9 +2835,14 @@ export interface ISuggestOptions { */ showSnippets?: boolean; /** - * Controls the visibility of the status bar at the bottom of the suggest widget. + * Status bar related settings. */ - hideStatusBar?: boolean; + statusBar?: { + /** + * Controls the visibility of the status bar at the bottom of the suggest widget. + */ + visible?: boolean; + } } export type InternalSuggestOptions = Readonly>; @@ -2878,7 +2884,9 @@ class EditorSuggest extends BaseEditorOption; + getAccessToken(): Thenable; accountName: string; } diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index a2ac8275f37..68c3275a7a4 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -338,9 +338,8 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri private gotoDefinition(position: Position, openToSide: boolean): Promise { this.editor.setPosition(position); - const definitionLinkOpensInPeek = this.editor.getOption(EditorOption.definitionLinkOpensInPeek); return this.editor.invokeWithinContext((accessor) => { - const canPeek = definitionLinkOpensInPeek && !this.isInPeekEditor(accessor); + const canPeek = !openToSide && this.editor.getOption(EditorOption.definitionLinkOpensInPeek) && !this.isInPeekEditor(accessor); const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { alias: '', label: '', id: '', precondition: undefined }); return action.run(accessor, this.editor); }); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 28aba650e6a..360986dd666 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -206,7 +206,8 @@ class RenameController implements IEditorContribution { this._bulkEditService.apply(renameResult, { editor: this.editor, showPreview: inputFieldResult.wantsPreview, - label: nls.localize('label', "Renaming '{0}'", loc?.text) + label: nls.localize('label', "Renaming '{0}'", loc?.text), + quotableLabel: nls.localize('quotableLabel', "Renaming {0}", loc?.text), }).then(result => { if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary)); diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 821cb7d2623..af01d6da5c5 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -234,7 +234,7 @@ flex-shrink: 0; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left > .monaco-icon-label { - max-width: 80%; + max-width: 100%; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .left > .monaco-icon-label { flex-shrink: 1; @@ -392,6 +392,10 @@ word-wrap: break-word; } +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs .codicon { + vertical-align: sub; +} + .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > p:empty { display: none; } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 5baa237e29a..91b862f2982 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -552,7 +552,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate toggleClass(this.element, 'with-status-bar', !this.editor.getOption(EditorOption.suggest).hideStatusBar); + const applyStatusBarStyle = () => toggleClass(this.element, 'with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); applyStatusBarStyle(); this.statusBarElement = append(this.element, $('.suggest-status-bar')); diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index ff7e6ef7fbc..f9e64360021 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -33,7 +33,7 @@ interface IEditorLayoutProviderOpts { readonly minimapSide: 'left' | 'right'; readonly minimapRenderCharacters: boolean; readonly minimapMaxColumn: number; - minimapMode?: 'actual' | 'cover' | 'contain'; + minimapSize?: 'proportional' | 'fill' | 'fit'; readonly pixelRatio: number; } @@ -47,7 +47,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.folding, false); const minimapOptions: EditorMinimapOptions = { enabled: input.minimap, - mode: input.minimapMode || 'actual', + size: input.minimapSize || 'proportional', side: input.minimapSide, renderCharacters: input.minimapRenderCharacters, maxColumn: input.minimapMaxColumn, @@ -978,7 +978,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'cover', + minimapSize: 'fill', pixelRatio: 2, }, { width: 1000, @@ -1042,7 +1042,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'cover', + minimapSize: 'fill', pixelRatio: 2, }, { width: 1000, @@ -1106,7 +1106,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'contain', + minimapSize: 'fit', pixelRatio: 2, }, { width: 1000, @@ -1170,7 +1170,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'contain', + minimapSize: 'fit', pixelRatio: 2, }, { width: 1000, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index fd704b03bd3..543ac0ca086 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3444,7 +3444,7 @@ declare namespace monaco.editor { * Control the minimap rendering mode. * Defaults to 'actual'. */ - mode?: 'actual' | 'cover' | 'contain'; + size?: 'proportional' | 'fill' | 'fit'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. @@ -3754,9 +3754,14 @@ declare namespace monaco.editor { */ showSnippets?: boolean; /** - * Controls the visibility of the status bar at the bottom of the suggest widget. + * Status bar related settings. */ - hideStatusBar?: boolean; + statusBar?: { + /** + * Controls the visibility of the status bar at the bottom of the suggest widget. + */ + visible?: boolean; + }; } export type InternalSuggestOptions = Readonly>; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 0428e1e888a..15b5c20cbbf 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -112,7 +112,7 @@ export class EnvironmentService implements IEnvironmentService { get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); } @memoize - get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, '.sync'); } + get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); } @memoize get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'settings.json'); } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index a81672c8311..f7f2b5a3d9e 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -173,6 +173,13 @@ export interface INotificationHandle { */ readonly onDidClose: Event; + /** + * Will be fired whenever the visibility of the notification changes. + * A notification can either be visible as toast or inside the notification + * center if it is visible. + */ + readonly onDidChangeVisibility: Event; + /** * Allows to indicate progress on the notification even after the * notification is already visible. diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 839f2c94ff9..1c8174147b0 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -240,9 +240,36 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase private async onDidStorageChangeExternal(): Promise { const items = await this.doGetItemsFromFile(); + // pervious cache, diff for changes + let changed = new Map(); + let deleted = new Set(); + if (this.cache) { + items.forEach((value, key) => { + const existingValue = this.cache?.get(key); + if (existingValue !== value) { + changed.set(key, value); + } + }); + + this.cache.forEach((_, key) => { + if (!items.has(key)) { + deleted.add(key); + } + }); + } + + // no previous cache, consider all as changed + else { + changed = items; + } + + // Update cache this.cache = items; - this._onDidChangeItemsExternal.fire({ items }); + // Emit as event as needed + if (changed.size > 0 || deleted.size > 0) { + this._onDidChangeItemsExternal.fire({ changed, deleted }); + } } async getItems(): Promise> { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index e2bbca21641..a6f2367f355 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -24,15 +24,16 @@ interface ISerializableUpdateRequest { } interface ISerializableItemsChangeEvent { - items: Item[]; + changed?: Item[]; + deleted?: Key[]; } export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel { private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; - private readonly _onDidChangeItems: Emitter = this._register(new Emitter()); - readonly onDidChangeItems: Event = this._onDidChangeItems.event; + private readonly _onDidChangeItems = this._register(new Emitter()); + readonly onDidChangeItems = this._onDidChangeItems.event; private whenReady: Promise; @@ -99,15 +100,18 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC } private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { - const items = new Map(); + const changed = new Map(); + const deleted = new Set(); events.forEach(event => { const existing = this.storageMainService.get(event.key); if (typeof existing === 'string') { - items.set(event.key, existing); + changed.set(event.key, existing); + } else { + deleted.add(event.key); } }); - return { items: mapToSerializable(items) }; + return { changed: mapToSerializable(changed), deleted: values(deleted) }; } listen(_: unknown, event: string): Event { @@ -170,8 +174,11 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS } private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void { - if (Array.isArray(e.items)) { - this._onDidChangeItemsExternal.fire({ items: serializableToMap(e.items) }); + if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { + this._onDidChangeItemsExternal.fire({ + changed: e.changed ? serializableToMap(e.changed) : undefined, + deleted: e.deleted ? new Set(e.deleted) : undefined + }); } } diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 1aed6216d8f..cf68e7b5aac 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; @@ -25,11 +25,11 @@ export class NativeStorageService extends Disposable implements IStorageService private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; private static readonly WORKSPACE_META_NAME = 'workspace.json'; - private readonly _onDidChangeStorage: Emitter = this._register(new Emitter()); - readonly onDidChangeStorage: Event = this._onDidChangeStorage.event; + private readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; - private readonly _onWillSaveState: Emitter = this._register(new Emitter()); - readonly onWillSaveState: Event = this._onWillSaveState.event; + private readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; private globalStorage: IStorage; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index ccbb2c55482..8568767aa3d 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -288,8 +288,8 @@ export class UndoRedoService implements IUndoRedoService { Severity.Info, nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label), [ - nls.localize('ok', "Undo in {0} files.", affectedEditStacks.length), - nls.localize('nok', "Undo this file."), + nls.localize('ok', "Undo In {0} Files", affectedEditStacks.length), + nls.localize('nok', "Undo This File"), nls.localize('cancel', "Cancel"), ], { diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 92590672d67..0dcf03eebde 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -68,7 +68,7 @@ export abstract class AbstractSynchroniser extends Disposable { ) { super(); this.syncFolder = joinPath(environmentService.userDataSyncHome, source); - this.lastSyncResource = joinPath(this.syncFolder, `.lastSync${source}.json`); + this.lastSyncResource = joinPath(this.syncFolder, `lastSync${source}.json`); this.cleanUpDelayer = new ThrottledDelayer(50); this.cleanUpBackup(); } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 52c6a90f94e..9e852eca701 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1500,7 +1500,7 @@ declare module 'vscode' { /** * A file system watcher notifies about changes to files and folders - * on disk. + * on disk or from other [FileSystemProviders](#FileSystemProvider). * * To get an instance of a `FileSystemWatcher` use * [createFileSystemWatcher](#workspace.createFileSystemWatcher). @@ -2020,7 +2020,7 @@ declare module 'vscode' { * Base kind for source actions: `source` * * Source code actions apply to the entire file. They must be explicitly requested and will not show in the - * normal [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions + * normal [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions * can be run on save using `editor.codeActionsOnSave` and are also shown in the `source` context menu. */ static readonly Source: CodeActionKind; @@ -2086,7 +2086,7 @@ declare module 'vscode' { /** * Requested kind of actions to return. * - * Actions not of this kind are filtered out before being shown by the lightbulb. + * Actions not of this kind are filtered out before being shown by the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action). */ readonly only?: CodeActionKind; } @@ -2138,7 +2138,15 @@ declare module 'vscode' { /** * Marks that the code action cannot currently be applied. * - * Disabled code actions will be surfaced in the refactor UI but cannot be applied. + * - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + * code action menu. + * + * - Disabled actions are shown as faded out in the code action menu when the user request a more specific type + * of code action, such as refactorings. + * + * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) + * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user a + * message with `reason` in the editor. */ disabled?: { /** @@ -2163,7 +2171,7 @@ declare module 'vscode' { /** * The code action interface defines the contract between extensions and - * the [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. + * the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * * A code action can be any command that is [known](#commands.getCommands) to the system. */ @@ -2495,16 +2503,18 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and - * the debug hover. + * the debug hover. In this contract the provider returns an evaluatable expression for a given position + * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. + * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * - * @param document The document in which the debug hover is opened. - * @param position The position in the document where the debug hover is opened. + * @param document The document for which the debug hover is about to appear. + * @param position The line and character position in the document where the debug hover is about to appear. * @param token A cancellation token. * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. @@ -9174,6 +9184,7 @@ declare module 'vscode' { /** * Register a provider that locates evaluatable expressions in text documents. + * VS Code will evaluate the expression in the active debug session and will show the result in the debug hover. * * If multiple providers are registered for a language an arbitrary provider will be used. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ff12027e949..a28f6d70c95 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -20,7 +20,7 @@ declare module 'vscode' { export interface AuthenticationSession { id: string; - accessToken(): Thenable; + getAccessToken(): Thenable; accountName: string; scopes: string[] } @@ -1273,14 +1273,14 @@ declare module 'vscode' { * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather * than cancelling it to ensure that VS Code has some valid backup. */ - backup(cancellation: CancellationToken): Thenable; + backup(cancellation: CancellationToken): Thenable; } /** * Represents a custom document for a custom webview editor. * - * Custom documents are only used within a given `WebviewCustomEditorProvider`. The lifecycle of a - * `WebviewEditorCustomDocument` is managed by VS Code. When more more references remain to a given `WebviewEditorCustomDocument` + * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a + * `CustomDocument` is managed by VS Code. When more more references remain to a given `CustomDocument` * then it is disposed of. * * @param UserDataType Type of custom object that extensions can store on the document. @@ -1297,7 +1297,7 @@ declare module 'vscode' { readonly uri: Uri; /** - * Event fired when there are no more references to the `WebviewEditorCustomDocument`. + * Event fired when there are no more references to the `CustomDocument`. */ readonly onDidDispose: Event; @@ -1313,7 +1313,7 @@ declare module 'vscode' { /** * Provider for webview editors that use a custom data model. * - * Custom webview editors use [`WebviewEditorCustomDocument`](#WebviewEditorCustomDocument) as their data model. + * Custom webview editors use [`CustomDocument`](#CustomDocument) as their data model. * This gives extensions full control over actions such as edit, save, and backup. * * You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text @@ -1321,20 +1321,22 @@ declare module 'vscode' { */ export interface CustomEditorProvider { /** - * Create the model for a given + * Resolve the model for a given resource. * - * @param document Resource being resolved. + * @param document Document to resolve. + * + * @return The capabilities of the resolved document. */ resolveCustomDocument(document: CustomDocument): Thenable; /** * Resolve a webview editor for a given resource. * - * To resolve a webview editor, a provider must fill in its initial html content and hook up all + * To resolve a webview editor, the provider must fill in its initial html content and hook up all * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. * - * @param document Document for resource being resolved. - * @param webviewPanel Webview being resolved. The provider should take ownership of this webview. + * @param document Document for the resource being resolved. + * @param webviewPanel Webview to resolve. The provider should take ownership of this webview. * * @return Thenable indicating that the webview editor has been resolved. */ @@ -1349,7 +1351,7 @@ declare module 'vscode' { * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. * * You should use text based webview editors when dealing with text based file formats, such as `xml` or `json`. - * For binary files or more specialized use cases, see [WebviewCustomEditorProvider](#WebviewCustomEditorProvider). + * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). */ export interface CustomTextEditorProvider { /** @@ -1359,7 +1361,7 @@ declare module 'vscode' { * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. * * @param document Resource being resolved. - * @param webviewPanel Webview being resolved. The provider should take ownership of this webview. + * @param webviewPanel Webview to resolve. The provider should take ownership of this webview. * * @return Thenable indicating that the webview editor has been resolved. */ @@ -1794,9 +1796,10 @@ declare module 'vscode' { * * The documentation is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. Note that in this case, we always pick the most specific - * documentation. For example, if documentation for both `Refactor` and `RefactorExtract` is provided, and we - * request code actions for `RefactorExtract`, we prefer the more specific documentation for `RefactorExtract`. + * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * most closely matches the requested code action kind. For example, if a provider has documentation for + * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, + * VS Code will use the documentation for `RefactorExtract` intead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. */ @@ -1817,7 +1820,10 @@ declare module 'vscode' { */ export interface OpenDialogOptions { /** - * Dialog title + * Dialog title. + * + * Depending on the underlying operating system this parameter might be ignored, since some + * systems do not present title on open dialogs. */ title?: string; } @@ -1827,7 +1833,10 @@ declare module 'vscode' { */ export interface SaveDialogOptions { /** - * Dialog title + * Dialog title. + * + * Depending on the underlying operating system this parameter might be ignored, since some + * systems do not present title on save dialogs. */ title?: string; } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22f91f6d2cc..15af9ba8bc6 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -25,7 +25,7 @@ export class MainThreadAuthenticationProvider { return { id: session.id, accountName: session.accountName, - accessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) + getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) }; }); } @@ -35,7 +35,7 @@ export class MainThreadAuthenticationProvider { return { id: session.id, accountName: session.accountName, - accessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) + getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) }; }); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e91e3341e57..c4bf866e3c2 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -631,7 +631,7 @@ export interface ExtHostWebviewsShape { $onSave(resource: UriComponents, viewType: string): Promise; $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise; - $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; } export interface ICellDto { diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 234de7348f3..b3b2d4d0f4a 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -34,7 +34,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi id: session.id, accountName: session.accountName, scopes: session.scopes, - accessToken: async () => { + getAccessToken: async () => { const isAllowed = await this._proxy.$getSessionsPrompt( this._provider.id, this.displayName, @@ -45,7 +45,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi throw new Error('User did not consent to token access.'); } - return session.accessToken(); + return session.getAccessToken(); } }; }); @@ -137,7 +137,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const sessions = await authProvider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { - return session.accessToken(); + return session.getAccessToken(); } throw new Error(`Unable to find session with id: ${sessionId}`); diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 87fefc82cfc..9db000d04f7 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -71,11 +71,17 @@ export class ExtHostTimeline implements IExtHostTimeline { scheme: scheme, onDidChange: undefined, async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) { - timelineDisposables.clear(); - // For now, only allow the caching of a single Uri - if (internalOptions?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) { - itemsBySourceByUriMap.clear(); + if (internalOptions?.cacheResults) { + if (options.cursor === undefined) { + timelineDisposables.clear(); + } + + if (!itemsBySourceByUriMap.has(getUriKey(uri))) { + itemsBySourceByUriMap.clear(); + } + } else { + timelineDisposables.clear(); } const result = await provider.provideTimeline(uri, options, token); diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 539f31df1c2..cfac95a00b3 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -727,7 +727,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return document._saveAs(URI.revive(targetResource)); } - async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { const document = this.getDocument(viewType, resourceComponents); return document._backup(cancellation); } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index f03e93dd95f..5cde8dd380a 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -553,7 +553,7 @@ export class MoveFocusedViewAction extends Action { const viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedViewId); if (!viewDescriptor || !viewDescriptor.canMoveView) { - this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable {0}.", focusedViewId)); + this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable.")); return Promise.resolve(); } @@ -613,7 +613,7 @@ export class MoveFocusedViewAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory, FocusedViewContext.notEqualsTo('')); // --- Resize View diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index cbc3d14705b..6e3ff5bf0f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -27,6 +27,10 @@ margin-bottom: auto; } +.monaco-workbench .activitybar > .content > .composite-bar-excess { + height: 100%; +} + .monaco-workbench .activitybar .menubar { width: 100%; height: 35px; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index f7f3e18a1b3..93102067fa2 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -17,13 +17,14 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { ITheme } from 'vs/platform/theme/common/themeService'; +import { LocalSelectionTransfer, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { Emitter } from 'vs/base/common/event'; import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { ICompositeDragAndDrop, CompositeDragAndDropData } from 'vs/base/parts/composite/browser/compositeDnd'; +import { IPaneComposite } from 'vs/workbench/common/panecomposite'; export interface ICompositeBarItem { id: string; @@ -38,7 +39,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { constructor( private viewDescriptorService: IViewDescriptorService, private targetContainerLocation: ViewContainerLocation, - private openComposite: (id: string, focus?: boolean) => void, + private openComposite: (id: string, focus?: boolean) => Promise, private moveComposite: (from: string, to: string) => void, private getVisibleCompositeIds: () => string[] ) { } @@ -53,8 +54,13 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (currentLocation !== this.targetContainerLocation && this.targetContainerLocation !== ViewContainerLocation.Panel) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); if (destinationContainer && !destinationContainer.rejectAddedViews) { - this.viewDescriptorService.moveViewsToContainer(this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView), destinationContainer); - this.openComposite(targetCompositeId, true); + const viewsToMove = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView); + this.viewDescriptorService.moveViewsToContainer(viewsToMove, destinationContainer); + this.openComposite(targetCompositeId, true).then(composite => { + if (composite && viewsToMove.length === 1) { + composite.openView(viewsToMove[0].id, true); + } + }); } } else { this.moveComposite(dragData.id, targetCompositeId); @@ -76,7 +82,11 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (destinationContainer && !destinationContainer.rejectAddedViews) { if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { this.viewDescriptorService.moveViewsToContainer([viewDescriptor], destinationContainer); - this.openComposite(targetCompositeId, true); + this.openComposite(targetCompositeId, true).then(composite => { + if (composite) { + composite.openView(viewDescriptor.id, true); + } + }); } else { this.viewDescriptorService.moveViewToLocation(viewDescriptor, this.targetContainerLocation); this.moveComposite(this.viewDescriptorService.getViewContainer(viewDescriptor.id)!.id, targetCompositeId); @@ -91,13 +101,25 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { this.moveComposite(newCompositeId, targetId); } - this.openComposite(newCompositeId, true); + this.openComposite(newCompositeId, true).then(composite => { + if (composite) { + composite.openView(viewDescriptor.id, true); + } + }); } } } } + onDragEnter(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean { + return this.canDrop(data, targetCompositeId); + } + onDragOver(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean { + return this.canDrop(data, targetCompositeId); + } + + private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean { const dragData = data.getData(); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); @@ -159,8 +181,6 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { const destinationContainer = viewContainerRegistry.get(targetCompositeId); return !!destinationContainer && !destinationContainer.rejectAddedViews; } - - return false; } } @@ -202,6 +222,7 @@ export class CompositeBar extends Widget implements ICompositeBar { constructor( items: ICompositeBarItem[], private options: ICompositeBarOptions, + @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -230,6 +251,7 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); + const excessDiv = parent.appendChild($('.composite-bar-excess')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { actionViewItemProvider: (action: IAction) => { @@ -256,58 +278,99 @@ export class CompositeBar extends Widget implements ICompositeBar { this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); // Allow to drop at the end to move composites to the end - this._register(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); + this._register(new DragAndDropObserver(excessDiv, { + onDragOver: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; - this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); - - this.options.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), undefined, e); - } - } - })); - - this._register(addDisposableListener(parent, EventType.DRAG_OVER, (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; + // Check if drop is allowed + if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e)) { + e.dataTransfer.dropEffect = 'none'; + } } } - } - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - EventHelper.stop(e, true); + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + EventHelper.stop(e, true); - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; + // Check if drop is allowed + if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), undefined, e)) { + e.dataTransfer.dropEffect = 'none'; + } } } - } + }, + + onDragEnter: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; + + // Check if drop is allowed + const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); + this.updateFromDragging(excessDiv, validDropTarget); + } + } + + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; + + // Check if drop is allowed + const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('view', draggedViewId), undefined, e); + this.updateFromDragging(excessDiv, validDropTarget); + } + } + }, + + onDragLeave: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || + this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + this.updateFromDragging(excessDiv, false); + } + }, + onDragEnd: (e: DragEvent) => { + // no-op, will not be called + }, + onDrop: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + + this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); + this.updateFromDragging(excessDiv, false); + } + } + + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; + this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); + + this.options.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), undefined, e); + this.updateFromDragging(excessDiv, false); + } + } + }, })); return actionBarDiv; @@ -412,6 +475,13 @@ export class CompositeBar extends Widget implements ICompositeBar { } } + private updateFromDragging(element: HTMLElement, isDragging: boolean): void { + const theme = this.themeService.getTheme(); + const dragBackground = this.options.colors(theme).dragAndDropBackground; + + element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : ''; + } + private resetActiveComposite(compositeId: string) { const defaultCompositeId = this.options.getDefaultCompositeId(); diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index c4eb0fbc8ed..f4d2bf15876 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -544,14 +544,16 @@ export class CompositeActionViewItem extends ActivityActionViewItem { if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); if (Array.isArray(data) && data[0].id !== this.activity.id) { - this.updateFromDragging(container, true); + const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', data[0].id), this.activity.id, e); + this.updateFromDragging(container, validDropTarget); } } if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); if (Array.isArray(data) && data[0].id !== this.activity.id) { - this.updateFromDragging(container, true); + const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('view', data[0].id), this.activity.id, e); + this.updateFromDragging(container, validDropTarget); } } }, diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 82301a0448e..ae63e201e24 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -26,10 +26,8 @@ export class ClearNotificationAction extends Action { super(id, label, 'codicon-close'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(CLEAR_NOTIFICATION, notification); - - return Promise.resolve(); } } @@ -46,10 +44,8 @@ export class ClearAllNotificationsAction extends Action { super(id, label, 'codicon-clear-all'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS); - - return Promise.resolve(); } } @@ -66,10 +62,8 @@ export class HideNotificationsCenterAction extends Action { super(id, label, 'codicon-chevron-down'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER); - - return Promise.resolve(); } } @@ -86,10 +80,8 @@ export class ExpandNotificationAction extends Action { super(id, label, 'codicon-chevron-up'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(EXPAND_NOTIFICATION, notification); - - return Promise.resolve(); } } @@ -106,10 +98,8 @@ export class CollapseNotificationAction extends Action { super(id, label, 'codicon-chevron-down'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification); - - return Promise.resolve(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 094c9e1567f..c37af327a36 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -100,6 +100,9 @@ export class NotificationsCenter extends Themable implements INotificationsCente // Theming this.updateStyles(); + // Mark as visible + this.model.notifications.forEach(notification => notification.updateVisibility(true)); + // Context Key this.notificationsCenterVisibleContextKey.set(true); @@ -115,7 +118,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente clearAllAction.enabled = false; } else { notificationsCenterTitle.textContent = localize('notifications', "Notifications"); - clearAllAction.enabled = true; + clearAllAction.enabled = this.model.notifications.some(notification => !notification.hasProgress); } } @@ -172,20 +175,22 @@ export class NotificationsCenter extends Themable implements INotificationsCente return; // only if visible } - let focusGroup = false; + let focusEditor = false; // Update notifications list based on event const [notificationsList, notificationsCenterContainer] = assertAllDefined(this.notificationsList, this.notificationsCenterContainer); switch (e.kind) { case NotificationChangeType.ADD: notificationsList.updateNotificationsList(e.index, 0, [e.item]); + e.item.updateVisibility(true); break; case NotificationChangeType.CHANGE: notificationsList.updateNotificationsList(e.index, 1, [e.item]); break; case NotificationChangeType.REMOVE: - focusGroup = isAncestor(document.activeElement, notificationsCenterContainer); + focusEditor = isAncestor(document.activeElement, notificationsCenterContainer); notificationsList.updateNotificationsList(e.index, 1); + e.item.updateVisibility(false); break; } @@ -197,7 +202,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente this.hide(); // Restore focus to editor group if we had focus - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -208,13 +213,16 @@ export class NotificationsCenter extends Themable implements INotificationsCente return; // already hidden } - const focusGroup = isAncestor(document.activeElement, this.notificationsCenterContainer); + const focusEditor = isAncestor(document.activeElement, this.notificationsCenterContainer); // Hide this._isVisible = false; removeClass(this.notificationsCenterContainer, 'visible'); this.notificationsList.hide(); + // Mark as hidden + this.model.notifications.forEach(notification => notification.updateVisibility(false)); + // Context Key this.notificationsCenterVisibleContextKey.set(false); @@ -222,7 +230,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente this._onDidChangeVisibility.fire(); // Restore focus to editor group if we had focus - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 413e700668d..b2797f261f3 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -75,6 +75,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Show Notifications Cneter CommandsRegistry.registerCommand(SHOW_NOTIFICATIONS_CENTER, () => { + toasts.hide(); center.show(); }); @@ -92,6 +93,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl if (center.isVisible) { center.hide(); } else { + toasts.hide(); center.show(); } }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 1bdde771ccb..cf45117e80c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -69,8 +69,9 @@ export class NotificationsStatus extends Disposable { } } + // Show the bell with a dot if there are unread or in-progress notifications const statusProperties: IStatusbarEntry = { - text: `${this.newNotificationsCount === 0 ? '$(bell)' : '$(bell-dot)'}${notificationsInProgress > 0 ? ' $(sync~spin)' : ''}`, + text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, tooltip: this.getTooltip(notificationsInProgress), showBeak: this.isNotificationsCenterVisible diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 95e287fbd49..ecee706fcb9 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -179,11 +179,8 @@ export class NotificationsToasts extends Themable implements INotificationsToast const toast: INotificationToast = { item, list: notificationList, container: notificationToastContainer, toast: notificationToast, toDispose: itemDisposables }; this.mapNotificationToToast.set(item, toast); - itemDisposables.add(toDisposable(() => { - if (this.isToastVisible(toast) && notificationsToastsContainer) { - notificationsToastsContainer.removeChild(toast.container); - } - })); + // When disposed, remove as visible + itemDisposables.add(toDisposable(() => this.updateToastVisibility(toast, false))); // Make visible notificationList.show(); @@ -236,6 +233,9 @@ export class NotificationsToasts extends Themable implements INotificationsToast addClass(notificationToast, 'notification-fade-in-done'); })); + // Mark as visible + item.updateVisibility(true); + // Events if (!this._isVisible) { this._isVisible = true; @@ -292,12 +292,13 @@ export class NotificationsToasts extends Themable implements INotificationsToast } private removeToast(item: INotificationViewItem): void { + let focusEditor = false; + const notificationToast = this.mapNotificationToToast.get(item); - let focusGroup = false; if (notificationToast) { const toastHasDOMFocus = isAncestor(document.activeElement, notificationToast.container); if (toastHasDOMFocus) { - focusGroup = !(this.focusNext() || this.focusPrevious()); // focus next if any, otherwise focus editor + focusEditor = !(this.focusNext() || this.focusPrevious()); // focus next if any, otherwise focus editor } // Listeners @@ -317,7 +318,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast this.doHide(); // Move focus back to editor group as needed - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -346,11 +347,11 @@ export class NotificationsToasts extends Themable implements INotificationsToast } hide(): void { - const focusGroup = this.notificationsToastsContainer ? isAncestor(document.activeElement, this.notificationsToastsContainer) : false; + const focusEditor = this.notificationsToastsContainer ? isAncestor(document.activeElement, this.notificationsToastsContainer) : false; this.removeToasts(); - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -459,12 +460,12 @@ export class NotificationsToasts extends Themable implements INotificationsToast notificationToasts.push(toast); break; case ToastVisibility.HIDDEN: - if (!this.isToastVisible(toast)) { + if (!this.isToastInDOM(toast)) { notificationToasts.push(toast); } break; case ToastVisibility.VISIBLE: - if (this.isToastVisible(toast)) { + if (this.isToastInDOM(toast)) { notificationToasts.push(toast); } break; @@ -530,7 +531,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast // In order to measure the client height, the element cannot have display: none toast.container.style.opacity = '0'; - this.setVisibility(toast, true); + this.updateToastVisibility(toast, true); heightToGive -= toast.container.offsetHeight; @@ -542,7 +543,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast } // Hide or show toast based on context - this.setVisibility(toast, makeVisible); + this.updateToastVisibility(toast, makeVisible); toast.container.style.opacity = ''; if (makeVisible) { @@ -551,20 +552,24 @@ export class NotificationsToasts extends Themable implements INotificationsToast }); } - private setVisibility(toast: INotificationToast, visible: boolean): void { - if (this.isToastVisible(toast) === visible) { + private updateToastVisibility(toast: INotificationToast, visible: boolean): void { + if (this.isToastInDOM(toast) === visible) { return; } + // Update visibility in DOM const notificationsToastsContainer = assertIsDefined(this.notificationsToastsContainer); if (visible) { notificationsToastsContainer.appendChild(toast.container); } else { notificationsToastsContainer.removeChild(toast.container); } + + // Update visibility in model + toast.item.updateVisibility(visible); } - private isToastVisible(toast: INotificationToast): boolean { + private isToastInDOM(toast: INotificationToast): boolean { return !!toast.container.parentElement; } } diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index abca35a41a7..5ac0818950c 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -62,6 +62,10 @@ } +.monaco-workbench .part.panel > .composite.title > .composite-bar-excess { + width: 100%; +} + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e8126ec13e1..a73f42b53f0 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -36,6 +36,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, IViewDescriptorCollection, ViewContainerLocation } from 'vs/workbench/common/views'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { IPaneComposite } from 'vs/workbench/common/panecomposite'; interface ICachedPanel { id: string; @@ -143,7 +144,7 @@ export class PanelPart extends CompositePart implements IPanelService { getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Panel, - (id: string, focus?: boolean) => this.openPanel(id, focus), + (id: string, focus?: boolean) => this.openPanel(id, focus) as Promise, (from: string, to: string) => this.compositeBar.move(from, to), () => this.getPinnedPanels().map(p => p.id) ), diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index bf954530159..21e312a5084 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -1009,7 +1009,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } if (!this.areExtensionsReady) { if (this.visibleViewsCountFromCache === undefined) { - return true; + // TODO @sbatten fix hack for #91367 + return this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel; } // Check in cache so that view do not jump. See #29609 return this.visibleViewsCountFromCache === 1; diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 73f0ef74053..809037bf68e 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -92,6 +92,9 @@ export class NotificationHandle extends Disposable implements INotificationHandl private readonly _onDidClose = this._register(new Emitter()); readonly onDidClose = this._onDidClose.event; + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) { super(); @@ -99,6 +102,11 @@ export class NotificationHandle extends Disposable implements INotificationHandl } private registerListeners(): void { + + // Visibility + this._register(this.item.onDidChangeVisibility(visible => this._onDidChangeVisibility.fire(visible))); + + // Closing Event.once(this.item.onDidClose)(() => { this._onDidClose.fire(); @@ -265,6 +273,7 @@ export interface INotificationViewItem { readonly onDidChangeExpansion: Event; readonly onDidClose: Event; + readonly onDidChangeVisibility: Event; readonly onDidChangeLabel: Event; expand(): void; @@ -275,6 +284,8 @@ export interface INotificationViewItem { updateMessage(message: NotificationMessage): void; updateActions(actions?: INotificationActions): void; + updateVisibility(visible: boolean): void; + close(): void; equals(item: INotificationViewItem): boolean; @@ -398,6 +409,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie private static readonly MAX_MESSAGE_LENGTH = 1000; private _expanded: boolean | undefined; + private _visible: boolean = false; private _actions: INotificationActions | undefined; private _progress: NotificationViewItemProgress | undefined; @@ -411,6 +423,9 @@ export class NotificationViewItem extends Disposable implements INotificationVie private readonly _onDidChangeLabel = this._register(new Emitter()); readonly onDidChangeLabel = this._onDidChangeLabel.event; + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + static create(notification: INotification, filter: NotificationsFilter = NotificationsFilter.OFF): INotificationViewItem | undefined { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { return undefined; // we need a message to show @@ -600,6 +615,14 @@ export class NotificationViewItem extends Disposable implements INotificationVie this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); } + updateVisibility(visible: boolean): void { + if (this._visible !== visible) { + this._visible = visible; + + this._onDidChangeVisibility.fire(visible); + } + } + expand(): void { if (this._expanded || !this.canCollapse) { return; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index d1b30384874..6b29b27222e 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -17,7 +17,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { flatten } from 'vs/base/common/arrays'; +import { flatten, mergeSort } from 'vs/base/common/arrays'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SetMap } from 'vs/base/common/collections'; @@ -212,9 +212,16 @@ export interface IViewDescriptorCollection extends IDisposable { readonly allViewDescriptors: IViewDescriptor[]; } +export enum ViewContentPriority { + Normal = 0, + Low = 1, + Lowest = 2 +} + export interface IViewContentDescriptor { readonly content: string; readonly when?: ContextKeyExpr | 'default'; + readonly priority?: ViewContentPriority; /** * ordered preconditions for each button in the content @@ -248,6 +255,13 @@ export interface IViewsRegistry { } function compareViewContentDescriptors(a: IViewContentDescriptor, b: IViewContentDescriptor): number { + const aPriority = a.priority ?? ViewContentPriority.Normal; + const bPriority = b.priority ?? ViewContentPriority.Normal; + + if (aPriority !== bPriority) { + return aPriority - bPriority; + } + return a.content < b.content ? -1 : 1; } @@ -329,8 +343,8 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { getViewWelcomeContent(id: string): IViewContentDescriptor[] { const result: IViewContentDescriptor[] = []; - result.sort(compareViewContentDescriptors); this._viewWelcomeContents.forEach(id, descriptor => result.push(descriptor)); + mergeSort(result, compareViewContentDescriptors); return result; } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts index 66624cf7bb6..faf9b538534 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts @@ -111,6 +111,7 @@ class BulkEditPreviewContribution { private async _previewEdit(edit: WorkspaceEdit) { this._ctxEnabled.set(true); + const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService); const view = await getBulkEditPane(this._viewsService); if (!view) { this._ctxEnabled.set(false); @@ -136,9 +137,9 @@ class BulkEditPreviewContribution { let session: PreviewSession; if (this._activeSession) { this._activeSession.cts.dispose(true); - session = new PreviewSession(this._activeSession.uxState); + session = new PreviewSession(uxState); } else { - session = new PreviewSession(new UXState(this._panelService, this._editorGroupsService)); + session = new PreviewSession(uxState); } this._activeSession = session; diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index fac092022e2..011918eb9b8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -79,7 +79,7 @@ export class ToggleColumnSelectionAction extends Action { } const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleColumnSelectionAction, ToggleColumnSelectionAction.ID, ToggleColumnSelectionAction.LABEL), 'View: Toggle Column Selection Mode', nls.localize('view', "View")); +registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleColumnSelectionAction, ToggleColumnSelectionAction.ID, ToggleColumnSelectionAction.LABEL), 'Toggle Column Selection Mode'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { group: '4_config', diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index a0e84261e3a..f14ffcf0a81 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -74,7 +74,7 @@ export interface ICustomEditorModel extends IWorkingCopy { readonly onWillSave: Event; readonly onWillSaveAs: Event; - onBackup(f: () => CancelablePromise): void; + onBackup(f: () => CancelablePromise): void; setDirty(dirty: boolean): void; undo(): void; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 6adac4c6199..43f887a82a0 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -29,7 +29,7 @@ namespace HotExitState { readonly type = Type.Pending; constructor( - public readonly operation: CancelablePromise, + public readonly operation: CancelablePromise, ) { } } @@ -90,9 +90,9 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel private readonly _onWillSaveAs = this._register(new Emitter()); public readonly onWillSaveAs = this._onWillSaveAs.event; - private _onBackup: undefined | (() => CancelablePromise); + private _onBackup: undefined | (() => CancelablePromise); - public onBackup(f: () => CancelablePromise) { + public onBackup(f: () => CancelablePromise) { if (this._onBackup) { throw new Error('Backup already implemented'); } @@ -182,7 +182,11 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._hotExitState = pendingState; try { - this._hotExitState = await pendingState.operation ? HotExitState.Allowed : HotExitState.NotAllowed; + await pendingState.operation; + // Make sure state has not changed in the meantime + if (this._hotExitState === pendingState) { + this._hotExitState = HotExitState.Allowed; + } } catch (e) { // Make sure state has not changed in the meantime if (this._hotExitState === pendingState) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index dec8ffa2385..e032e135584 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2669,7 +2669,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { if (server) { this.tooltip = localize('Install in other server to enable', "Install the extension on '{0}' to enable.", server.label); } else { - this.tooltip = localize('disabled because of extension kind', "This extension cannot be enabled in the remote server."); + this.tooltip = localize('disabled because of extension kind', "This extension has defined that it cannot run on the remote server"); } return; } diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index e7e32d6ff20..04e0bf24825 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -55,11 +55,11 @@ align-items: center; } -.explorer-viewlet .pane-header .monaco-count-badge.hidden { +.pane-header .dirty-count.monaco-count-badge.hidden { display: none; } -.explorer-viewlet .monaco-count-badge { +.dirty-count.monaco-count-badge { padding: 1px 6px 2px; margin-left: 6px; min-height: auto; diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c388727aba4..9c5162d1999 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -185,7 +185,7 @@ export class OpenEditorsView extends ViewPane { super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); - this.dirtyCountElement = dom.append(count, $('.monaco-count-badge')); + this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge')); this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index abd5d1d00fe..fd2523ba4d9 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -856,6 +856,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); if (linkFg) { - collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link span:hover { color: ${linkFg}; }`); + collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link .marker-code > span:hover { color: ${linkFg}; }`); + collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-list:focus .monaco-tl-contents .details-container a.code-link .marker-code > span:hover { color: ${linkFg.lighten(.4)}; }`); } }); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index c1a674828f9..bae1aee1cd5 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1238,7 +1238,10 @@ export class SettingTreeRenderers { private getActionsForSetting(setting: ISetting): IAction[] { const enableSync = this._userDataSyncEnablementService.isEnabled(); return enableSync && !setting.disallowSyncIgnore ? - [this._instantiationService.createInstance(SyncSettingAction, setting)] : + [ + new Separator(), + this._instantiationService.createInstance(SyncSettingAction, setting) + ] : []; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index b78b35610e7..1d3fdd35ad5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -44,7 +44,7 @@ export interface ISpliceEvent { export class EmptyPane extends ViewPane { static readonly ID = 'workbench.scm'; - static readonly TITLE = localize('scm providers', "Source Control Providers"); + static readonly TITLE = localize('scm', "Source Control"); constructor( options: IViewPaneOptions, diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 3de68eda5a0..7de8978e745 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -22,3 +22,5 @@ export const SearchEditorScheme = 'search-editor'; export const SearchEditorBodyScheme = 'search-editor-body'; export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; + +export const SearchEditorID = 'workbench.editor.searchEditor'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 57b857a860d..eb2646630b4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -9,7 +9,7 @@ import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,11 +20,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand, RerunSearchEditorSearchAction } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -159,15 +159,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: selectAllSearchEditorMatchesCommand }); -CommandsRegistry.registerCommand( - SearchEditorConstants.RerunSearchEditorSearchCommandId, - (accessor: ServicesAccessor) => { - const activeControl = accessor.get(IEditorService).activeControl; - if (activeControl instanceof SearchEditor) { - activeControl.triggerSearch({ resetCursor: false }); - } - }); - CommandsRegistry.registerCommand( SearchEditorConstants.CleanSearchEditorStateCommandId, (accessor: ServicesAccessor) => { @@ -191,4 +182,19 @@ registry.registerWorkbenchAction( registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL), 'Search Editor: Open New Search Editor', category); + +registry.registerWorkbenchAction(SyncActionDescriptor.create(RerunSearchEditorSearchAction, RerunSearchEditorSearchAction.ID, RerunSearchEditorSearchAction.LABEL, + { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R } }, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)), + 'Search Editor: Rerun', category); //#endregion + + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: RerunSearchEditorSearchAction.ID, + title: RerunSearchEditorSearchAction.LABEL, + icon: { id: 'codicon/refresh' }, + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID)) +}); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 8f26a359018..523e97cdac5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -35,7 +35,7 @@ import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; -import { InSearchEditor, SearchEditorFindMatchClass } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { InSearchEditor, SearchEditorFindMatchClass, SearchEditorID } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { extractSearchQuery, serializeSearchConfiguration, serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; @@ -56,7 +56,7 @@ const FILE_LINE_REGEX = /^(\S.*):$/; type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; export class SearchEditor extends BaseTextEditor { - static readonly ID: string = 'workbench.editor.searchEditor'; + static readonly ID: string = SearchEditorID; static readonly SEARCH_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'searchEditorViewState'; @@ -194,6 +194,7 @@ export class SearchEditor extends BaseTextEditor { const runAgainLink = DOM.append(this.messageBox, DOM.$('a.pointer.prominent.message', {}, localize('runSearch', "Run Search"))); this.messageDisposables.push(DOM.addDisposableListener(runAgainLink, DOM.EventType.CLICK, async () => { await this.triggerSearch(); + this.searchResultEditor.focus(); this.toggleRunAgainMessage(false); })); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 094acf25fd3..464d2bb6e51 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -116,6 +116,24 @@ export class OpenResultsInEditorAction extends Action { } } +export class RerunSearchEditorSearchAction extends Action { + static readonly ID: string = Constants.RerunSearchEditorSearchCommandId; + static readonly LABEL = localize('search.rerunSearchInEditor', "Search Again"); + + constructor(id: string, label: string, + @IEditorService private readonly editorService: IEditorService, + ) { + super(id, label, 'codicon-refresh'); + } + + async run() { + const input = this.editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (this.editorService.activeControl as SearchEditor).triggerSearch({ resetCursor: false }); + } + } +} + const openNewSearchEditor = async (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 389faee3931..038519308e4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -216,7 +216,7 @@ export const serializeSearchResultForEditor = const info = [ searchResult.count() - ? `${filecount} - ${resultcount}` + ? `${resultcount} - ${filecount}` : localize('noResults', "No Results"), '']; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index d438a1b739c..29dceaaca3d 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -251,7 +251,7 @@ export class TimelinePane extends ViewPane { } this._tree.setChildren(null, undefined); - this.message = localize('timeline.loading', 'Loading timeline for ${0}...', basename(uri.fsPath)); + this.message = localize('timeline.loading', 'Loading timeline for {0}...', basename(uri.fsPath)); }, 500, this._uri); } } @@ -291,7 +291,7 @@ export class TimelinePane extends ViewPane { if (!reset) { // TODO: Handle pending request - if (cursors?.more === false) { + if (cursors?.more !== true) { continue; } @@ -306,6 +306,10 @@ export class TimelinePane extends ViewPane { request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true } )!; + if (request === undefined) { + continue; + } + this._pendingRequests.set(source, request); if (!reusingToken) { request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); @@ -322,6 +326,10 @@ export class TimelinePane extends ViewPane { new CancellationTokenSource(), { cacheResults: true } )!; + if (request === undefined) { + continue; + } + this._pendingRequests.set(source, request); request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 8bf9147ddcb..aeb8ef22ab9 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -226,7 +226,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (account) { try { - const token = await account.accessToken(); + const token = await account.getAccessToken(); this.authTokenService.setToken(token); this.authenticationState.set(AuthStatus.SignedIn); } catch (e) { @@ -522,7 +522,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo quickPick.ok = false; quickPick.customButton = true; if (this.authenticationState.get() === AuthStatus.SignedIn) { - quickPick.customLabel = localize('turn on', "Turn on"); + quickPick.customLabel = localize('turn on', "Turn On"); } else { const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName); @@ -552,8 +552,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const disposables: DisposableStore = new DisposableStore(); const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, description?: string, detail?: string }>(); - const chooseAnotherItemId = 'chooseAnother'; disposables.add(quickPick); + const chooseAnotherItemId = 'chooseAnother'; quickPick.title = localize('pick account', "{0}: Pick an account", displayName); quickPick.ok = false; quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); @@ -680,7 +680,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo type: 'info', message: localize('turn off sync confirmation', "Turn off Sync"), detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."), - primaryButton: localize('turn off', "Turn off"), + primaryButton: localize('turn off', "Turn Off"), checkbox: { label: localize('turn off sync everywhere', "Turn off sync on all your devices and clear the data from the cloud.") } @@ -916,7 +916,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return new Promise((c, e) => { const quickInputService = accessor.get(IQuickInputService); const commandService = accessor.get(ICommandService); + const disposables = new DisposableStore(); const quickPick = quickInputService.createQuickPick(); + disposables.add(quickPick); const items: Array = []; if (that.userDataSyncService.conflictsSources.length) { for (const source of that.userDataSyncService.conflictsSources) { @@ -937,7 +939,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ type: 'separator' }); items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService) }); quickPick.items = items; - const disposables = new DisposableStore(); disposables.add(quickPick.onDidAccept(() => { if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) { commandService.executeCommand(quickPick.selectedItems[0].id); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts index 51fb3194e95..e7e50346aa5 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts @@ -7,9 +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 } from './viewsWelcomeExtensionPoint'; +import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor, ViewIdentifierMap } from './viewsWelcomeExtensionPoint'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IViewsRegistry, ViewContentPriority } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -45,9 +45,11 @@ export class ViewsWelcomeContribution extends Disposable implements IWorkbenchCo } for (const welcome of contribution.value) { - const disposable = viewsRegistry.registerViewWelcomeContent(welcome.view, { + const id = ViewIdentifierMap[welcome.view] ?? welcome.view; + const disposable = viewsRegistry.registerViewWelcomeContent(id, { content: welcome.contents, - when: ContextKeyExpr.deserialize(welcome.when) + when: ContextKeyExpr.deserialize(welcome.when), + priority: contribution.description.isBuiltin ? ViewContentPriority.Low : ViewContentPriority.Lowest }); this.viewWelcomeContents.set(welcome, disposable); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts index 28a5bc02dfa..9a1fdbe078e 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts @@ -20,9 +20,15 @@ export interface ViewWelcome { export type ViewsWelcomeExtensionPoint = ViewWelcome[]; +export const ViewIdentifierMap: { [key: string]: string } = { + 'explorer': 'workbench.explorer.emptyView', + 'debug': 'workbench.debug.startView', + 'scm': 'workbench.scm', +}; + const viewsWelcomeExtensionPointSchema = Object.freeze({ type: 'array', - description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content."), + description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content. Welcome content will be rendered in views whenever they have no meaningful content to display, ie. the File Explorer when no folder is open. Such content is useful as in-product documentation to drive users to use certain features before they are available. A good example would be a `Clone Repository` button in the File Explorer welcome view."), items: { type: 'object', description: nls.localize('contributes.viewsWelcome.view', "Contributed welcome content for a specific view."), @@ -33,15 +39,16 @@ const viewsWelcomeExtensionPointSchema = Object.freeze { return { ariaSummary: bulkEdit.ariaMessage() }; }).catch(err => { diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index e9e8c8fff36..58c9355d126 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -156,7 +156,7 @@ export class SimpleFileDialog { public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.remotePathService.userHome; + this.userHome = await this.getUserHome(); const newOptions = this.getOptions(options); if (!newOptions) { return Promise.resolve(undefined); @@ -167,7 +167,7 @@ export class SimpleFileDialog { public async showSaveDialog(options: ISaveDialogOptions): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.remotePathService.userHome; + this.userHome = await this.getUserHome(); this.requiresTrailing = true; const newOptions = this.getOptions(options, true); if (!newOptions) { @@ -231,6 +231,13 @@ export class SimpleFileDialog { return this.remoteAgentEnvironment; } + private async getUserHome(): Promise { + if (this.scheme !== Schemas.file) { + return this.remotePathService.userHome; + } + return URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); + } + private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index b7ee69dc594..c94ee4e88cf 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -137,7 +137,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } @memoize - get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, '.sync'); } + get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } @memoize get settingsSyncPreviewResource(): URI { return joinPath(this.userDataSyncHome, 'settings.json'); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 4a07843244d..9f8c6ac6f5e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -209,7 +209,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); } if (!isLanguagePackExtension(manifest) && !canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) { - const error = new Error(localize('cannot be installed', "Cannot install '{0}' extension since it cannot be enabled in the remote server.", gallery.displayName || gallery.name)); + const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; return Promise.reject(error); } diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 6d4723b34e3..2f464fb7405 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/progressService'; import { localize } from 'vs/nls'; -import { IDisposable, dispose, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, DisposableStore, MutableDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProgressService, IProgressOptions, IProgressStep, ProgressLocation, IProgress, Progress, IProgressCompositeOptions, IProgressNotificationOptions, IProgressRunner, IProgressIndicator, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusbarAlignment, IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar'; @@ -24,6 +24,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventHelper } from 'vs/base/browser/dom'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { parseLinkedText } from 'vs/base/common/linkedText'; export class ProgressService extends Disposable implements IProgressService { @@ -191,6 +192,46 @@ export class ProgressService extends Disposable implements IProgressService { } }; + const createWindowProgress = () => { + + // Create a promise that we can resolve as needed + // when the outside calls dispose on us + let promiseResolve: () => void; + const promise = new Promise(resolve => promiseResolve = resolve); + + this.withWindowProgress({ + location: ProgressLocation.Window, + title: options.title ? parseLinkedText(options.title).toString() : undefined, // convert markdown links => string + command: 'notifications.showList' + }, progress => { + + function reportProgress(step: IProgressStep) { + if (step.message) { + progress.report({ + message: parseLinkedText(step.message).toString() // convert markdown links => string + }); + } + } + + // Apply any progress that was made already + if (progressStateModel.step) { + reportProgress(progressStateModel.step); + } + + // Continue to report progress as it happens + const onDidReportListener = progressStateModel.onDidReport(step => reportProgress(step)); + promise.finally(() => onDidReportListener.dispose()); + + // When the progress model gets disposed, we are done as well + Event.once(progressStateModel.onDispose)(() => promiseResolve()); + + return promise; + }); + + // Dispose means completing our promise + return toDisposable(() => promiseResolve()); + }; + const createNotification = (message: string, increment?: number): INotificationHandle => { const notificationDisposables = new DisposableStore(); @@ -229,7 +270,7 @@ export class ProgressService extends Disposable implements IProgressService { primaryActions.push(cancelAction); } - const handle = this.notificationService.notify({ + const notification = this.notificationService.notify({ severity: Severity.Info, message, source: options.source, @@ -237,12 +278,26 @@ export class ProgressService extends Disposable implements IProgressService { progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true } }); - updateProgress(handle, increment); + // Switch to window based progress once the notification + // changes visibility to hidden and is still ongoing. + // Remove that window based progress once the notification + // shows again. + let windowProgressDisposable: IDisposable | undefined = undefined; + notificationDisposables.add(notification.onDidChangeVisibility(visible => { + + // Clear any previous running window progress + dispose(windowProgressDisposable); + + // Create new window progress if notification got hidden + if (!visible && !progressStateModel.done) { + windowProgressDisposable = createWindowProgress(); + } + })); // Clear upon dispose - Event.once(handle.onDidClose)(() => notificationDisposables.dispose()); + Event.once(notification.onDidClose)(() => notificationDisposables.dispose()); - return handle; + return notification; }; const updateProgress = (notification: INotificationHandle, increment?: number): void => { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 67332750e50..949e69d12ef 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -919,6 +919,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } dispose(): void { + this.logService.trace('[text file model] dispose()', this.resource.toString(true)); + this.disposed = true; this.inConflictMode = false; this.inOrphanMode = false; diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 7eefd25cb8e..a050bc29422 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -98,6 +98,17 @@ suite('Notifications', () => { assert.equal(called, 1); + called = 0; + item1.onDidChangeVisibility(e => { + called++; + }); + + item1.updateVisibility(true); + item1.updateVisibility(false); + item1.updateVisibility(false); + + assert.equal(called, 2); + called = 0; item1.onDidClose(() => { called++; diff --git a/test/smoke/README.md b/test/smoke/README.md index 8b41d05ed08..7bec799f89d 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -13,13 +13,13 @@ yarn --cwd test/automation yarn smoketest # Dev (Web) -yarn smoketest --web --browser [chromium|firefox|webkit] +yarn smoketest --web --browser [chromium|webkit] # Build (Electron) yarn smoketest --build --stable-build # Build (Web - read instructions below) -yarn smoketest --build --web --browser [chromium|firefox|webkit] +yarn smoketest --build --web --browser [chromium|webkit] # Remote (Electron) yarn smoketest --build --remote diff --git a/test/smoke/src/areas/css/css.test.ts b/test/smoke/src/areas/css/css.test.ts deleted file mode 100644 index aa6ccb9590b..00000000000 --- a/test/smoke/src/areas/css/css.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Application, ProblemSeverity, Problems } from '../../../../automation'; - -export function setup() { - describe('CSS', () => { - it('verifies quick outline', async function () { - const app = this.app as Application; - await app.workbench.quickopen.openFile('style.css'); - - await app.workbench.quickopen.openQuickOutline(); - await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 2); - }); - - it('verifies warnings for the empty rule', async function () { - const app = this.app as Application; - await app.workbench.quickopen.openFile('style.css'); - await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}'); - - await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.WARNING)); - - await app.workbench.problems.showProblemsView(); - await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.WARNING)); - await app.workbench.problems.hideProblemsView(); - }); - - it('verifies that warning becomes an error once setting changed', async function () { - const app = this.app as Application; - await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"'); - await app.workbench.quickopen.openFile('style.css'); - - await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.ERROR)); - - const problems = new Problems(app.code); - await problems.showProblemsView(); - await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.ERROR)); - await problems.hideProblemsView(); - }); - }); -} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index a1273ba379d..fc1f4e5569f 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -25,7 +25,6 @@ import { setup as setupDataMigrationTests } from './areas/workbench/data-migrati import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test'; import { setup as setupDataSearchTests } from './areas/search/search.test'; -import { setup as setupDataCSSTests } from './areas/css/css.test'; import { setup as setupDataEditorTests } from './areas/editor/editor.test'; import { setup as setupDataStatusbarTests } from './areas/statusbar/statusbar.test'; import { setup as setupDataExtensionTests } from './areas/extensions/extensions.test'; @@ -302,7 +301,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web) { setupDataLossTests(); } if (!opts.web) { setupDataPreferencesTests(); } setupDataSearchTests(); - setupDataCSSTests(); setupDataEditorTests(); setupDataStatusbarTests(!!opts.web); if (!opts.web) { setupDataExtensionTests(); } diff --git a/test/unit/README.md b/test/unit/README.md index 4172285834f..f471ebb7d8e 100644 --- a/test/unit/README.md +++ b/test/unit/README.md @@ -9,6 +9,8 @@ All unit tests are run inside a electron-browser environment which access to DOM - 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 +For instance, `./scripts/test.sh --debug --glob **/extHost*.test.js` runs all tests from `extHost`-files and enables you to debug them. + ## Run (inside browser) yarn test-browser --browser webkit --browser chromium @@ -24,11 +26,6 @@ Unit tests from layers `common` and `browser` are run inside `chromium`, `webkit yarn run mocha --run src/vs/editor/test/browser/controller/cursor.test.ts -## Debug - -To debug tests use `--debug` when running the test script. Also, the set of tests can be reduced with the `--run` and `--runGlob` flags. Both require a file path/pattern. Like so: - - ./scripts/test.sh --debug --runGrep **/extHost*.test.js ## Coverage