From a8c73b490d223b58a9f073123c6ee4c9c25751c0 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Wed, 11 Oct 2023 23:55:59 +0100 Subject: [PATCH 01/21] Show `Reload Required` buttons simultaneously after updateAllExtensions (#163627) --- .../browser/extensions.contribution.ts | 16 +++++++------- .../browser/extensionsWorkbenchService.ts | 21 ++++++++++++++++++- .../contrib/extensions/common/extensions.ts | 3 ++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 77e04f41e31..c5c82fc5736 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,7 +13,7 @@ import { EnablementState, IExtensionManagementServerService, IWorkbenchExtension import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -672,14 +672,14 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } ], icon: installWorkspaceRecommendedIcon, - run: () => { - return Promise.all(this.extensionsWorkbenchService.outdated.map(async extension => { - try { - await this.extensionsWorkbenchService.install(extension, extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined); - } catch (err) { - runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err)); + run: async () => { + const results = await this.extensionsWorkbenchService.updateAll(); + results.forEach((result) => { + if (result.error && result.context?.extension) { + const extension: IExtension = result.context.extension; + runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); } - })); + }); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 154696503e3..ae61ff5b04d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, - IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible + IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -1364,6 +1364,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } + async updateAll(): Promise { + const toUpdate: InstallExtensionInfo[] = []; + this.outdated.forEach((extension) => { + if (extension.gallery) { + toUpdate.push({ + extension: extension.gallery, + options: { + operation: InstallOperation.Update, + installPreReleaseVersion: extension.local?.isPreReleaseVersion, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + donotIncludePackAndDependencies: true, + context: { extension }, // Provide extension as context in result so result reporting can use its properties + } + }); + } + }); + return this.extensionManagementService.installGalleryExtensions(toUpdate); + } + private async syncInstalledExtensionsWithGallery(gallery: IGalleryExtension[]): Promise { const extensions: Extensions[] = []; if (this.localExtensions) { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 1ce6153fb8b..5582a39fea9 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; -import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -116,6 +116,7 @@ export interface IExtensionsWorkbenchService { open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; + updateAll(): Promise; // Sync APIs isExtensionIgnoredToSync(extension: IExtension): boolean; From fb52ff280748d4d2407a514d8e41296296eb33a4 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Wed, 11 Oct 2023 23:57:34 +0100 Subject: [PATCH 02/21] Hide download count and star rating while (un)installing or updating --- .../workbench/contrib/extensions/browser/extensionsWidgets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index f920b7496b6..5445e6230a1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -82,7 +82,7 @@ export class InstallCountWidget extends ExtensionWidget { return; } - if (this.small && this.extension.state === ExtensionState.Installed) { + if (this.small && this.extension.state !== ExtensionState.Uninstalled) { return; } @@ -146,7 +146,7 @@ export class RatingsWidget extends ExtensionWidget { return; } - if (this.small && this.extension.state === ExtensionState.Installed) { + if (this.small && this.extension.state !== ExtensionState.Uninstalled) { return; } From a67801be78b18e0216f631d0ebefde24208ffeec Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 19 Oct 2023 17:51:05 +0800 Subject: [PATCH 03/21] fix: Close #195980, Fix contribute points default display for extensions --- src/vs/workbench/contrib/extensions/browser/extensionEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 9ee8986fd53..489aa98d108 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -1237,7 +1237,7 @@ export class ExtensionEditor extends EditorPane { return $('tr', undefined, $('td', undefined, $('code', undefined, key)), $('td', undefined, description), - $('td', undefined, $('code', undefined, `${isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default}`))); + $('td', undefined, $('code', undefined, `${isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : JSON.stringify(properties[key].default)}`))); }) ) ); From bb515b4afac2c5e08b24b80e99bc5c46585677de Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Thu, 19 Oct 2023 22:16:21 +0100 Subject: [PATCH 04/21] Pass identifier rather than extension object in result context --- .../extensions/browser/extensions.contribution.ts | 9 ++++++--- .../extensions/browser/extensionsWorkbenchService.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c5c82fc5736..ca4107fb309 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -673,11 +673,14 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi ], icon: installWorkspaceRecommendedIcon, run: async () => { + const outdated = this.extensionsWorkbenchService.outdated; const results = await this.extensionsWorkbenchService.updateAll(); results.forEach((result) => { - if (result.error && result.context?.extension) { - const extension: IExtension = result.context.extension; - runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); + if (result.error && result.context?.extensionIdentifier) { + const extension: IExtension | undefined = outdated.find((extension) => extension.identifier === result.context?.extensionIdentifier); + if (extension) { + runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); + } } }); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ae61ff5b04d..7e2a2660d0b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1375,7 +1375,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installPreReleaseVersion: extension.local?.isPreReleaseVersion, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, donotIncludePackAndDependencies: true, - context: { extension }, // Provide extension as context in result so result reporting can use its properties + context: { extensionIdentifier: extension.identifier }, // Provide this in result so result reporting can look it up and use its properties } }); } From 4d775879c7a56e4db5b7d5b411b8e35f0a7535a5 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Sat, 21 Oct 2023 12:00:54 +0100 Subject: [PATCH 05/21] Changes after feedback --- .../contrib/extensions/browser/extensions.contribution.ts | 4 ++-- .../contrib/extensions/browser/extensionsWorkbenchService.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index ca4107fb309..3b7f3a69780 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -676,8 +676,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi const outdated = this.extensionsWorkbenchService.outdated; const results = await this.extensionsWorkbenchService.updateAll(); results.forEach((result) => { - if (result.error && result.context?.extensionIdentifier) { - const extension: IExtension | undefined = outdated.find((extension) => extension.identifier === result.context?.extensionIdentifier); + if (result.error) { + const extension: IExtension | undefined = outdated.find((extension) => areSameExtensions(extension.identifier, result.identifier)); if (extension) { runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7e2a2660d0b..3bb04e433a2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1374,8 +1374,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension operation: InstallOperation.Update, installPreReleaseVersion: extension.local?.isPreReleaseVersion, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, - donotIncludePackAndDependencies: true, - context: { extensionIdentifier: extension.identifier }, // Provide this in result so result reporting can look it up and use its properties } }); } From 9b9187dd4998212012f3ecac3bf6110985d8cb2a Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Sun, 22 Oct 2023 21:52:13 -0700 Subject: [PATCH 06/21] Fix inline chat command title --- .../contrib/chat/browser/actions/chatAccessibilityHelp.ts | 2 +- .../workbench/contrib/inlineChat/browser/inlineChatActions.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 51b5f469841..c787fbdaa98 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -31,7 +31,7 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'pane } else { const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Code Chat ({0}).", startChatKeybinding)); + content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat ({0}).", startChatKeybinding)); const upHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.previousFromHistory')?.getAriaLabel(); const downHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.nextFromHistory')?.getAriaLabel(); if (upHistoryKeybinding && downHistoryKeybinding) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 2f2cb558da9..fa7769443e9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -37,7 +37,7 @@ export class StartSessionAction extends EditorAction2 { constructor() { super({ id: 'inlineChat.start', - title: { value: localize('run', 'Start Code Chat'), original: 'Start Code Chat' }, + title: { value: localize('run', 'Start Inline Chat'), original: 'Start Inline Chat' }, category: AbstractInlineChatAction.category, f1: true, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable), @@ -64,7 +64,7 @@ export class UnstashSessionAction extends EditorAction2 { constructor() { super({ id: 'inlineChat.unstash', - title: { value: localize('unstash', 'Resume Last Dismissed Code Chat'), original: 'Resume Last Dismissed Code Chat' }, + title: { value: localize('unstash', 'Resume Last Dismissed Inline Chat'), original: 'Resume Last Dismissed Inline Chat' }, category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_STASHED_SESSION, EditorContextKeys.writable), keybinding: { From 67df0670baef3d9e2b4c53b5ec7db3b47f9545c1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:19:52 +0200 Subject: [PATCH 07/21] Git - set action button command argument (#196216) --- extensions/git/src/commitMessageProvider.ts | 18 ++++++++++++++---- .../contrib/scm/browser/scmViewPane.ts | 2 -- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/extensions/git/src/commitMessageProvider.ts b/extensions/git/src/commitMessageProvider.ts index 29f2da8c8b1..c774ea5dcec 100644 --- a/extensions/git/src/commitMessageProvider.ts +++ b/extensions/git/src/commitMessageProvider.ts @@ -20,7 +20,9 @@ export class TestCommitMessageProvider implements CommitMessageProvider { readonly icon = new ThemeIcon('rocket'); readonly title = 'Generate Commit Message (Test)'; - async provideCommitMessage(_: ApiRepository, __: string[], token: CancellationToken): Promise { + async provideCommitMessage(repository: ApiRepository, _: string[], token: CancellationToken): Promise { + console.log('Repository: ', repository.rootUri.fsPath); + if (token.isCancellationRequested) { return undefined; } @@ -62,12 +64,20 @@ export class GenerateCommitMessageActionButton { return this.state.isGenerating ? { icon: new ThemeIcon('debug-stop'), - command: { title: l10n.t('Cancel'), command: 'git.generateCommitMessageCancel' }, - enabled: this.state.enabled + command: { + title: l10n.t('Cancel'), + command: 'git.generateCommitMessageCancel', + arguments: [this.repository.sourceControl] + }, + enabled: this.state.enabled, } : { icon: this.commitMessageProviderRegistry.commitMessageProvider.icon ?? new ThemeIcon('sparkle'), - command: { title: this.commitMessageProviderRegistry.commitMessageProvider.title, command: 'git.generateCommitMessage' }, + command: { + title: this.commitMessageProviderRegistry.commitMessageProvider.title, + command: 'git.generateCommitMessage', + arguments: [this.repository.sourceControl] + }, enabled: this.state.enabled }; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a9cb19a3718..ce8d71d993e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1959,8 +1959,6 @@ class SCMInputWidget { updateEnablement(input.enabled); // ActionBar - this.actionBar.context = input.repository.provider; - const onDidChangeActionButton = () => { this.actionBar.clear(); if (!input.actionButton) { From d31d5ca86a298eed6e18db6e77200accd9cb944d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Oct 2023 09:12:30 +0200 Subject: [PATCH 08/21] voice - stop recording animation with reduced motion enabled (#196230) --- .../actions/media/voiceChatActions.css | 14 +++++++++++--- .../electron-sandbox/actions/voiceChatActions.ts | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index 1f105070d1e..44a67e2efdf 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -12,17 +12,25 @@ } /* - * Clear animation styles when hovering. + * Clear animation styles when hovering or when reduced motion is enabled. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover, .monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover { animation: none; } +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + animation: none; +} /* - * Replace with "stop" icon when hovering. + * Replace with "stop" icon when hovering or when reduced motion is enabled. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover::before, .monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover::before { - content: "\ead7"; /* use `debug-stop` icon unicode for hovering over running voice recording */ + content: "\ead7"; +} +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + content: "\ead7"; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 91ac2dcef34..787443bd275 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -683,8 +683,8 @@ registerThemingParticipant((theme, collector) => { // Show a "microphone" icon when recording is in progress that glows via outline. collector.addRule(` - .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):not(:hover), - .monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):not(:hover) { + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):not(:hover), + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):not(:hover) { color: ${activeRecordingColor}; outline: 1px solid ${activeRecordingColor}; outline-offset: -1px; From 8b6ae4ade6fc43ce21e0004fcaf7885fc6ed8b99 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Oct 2023 09:12:50 +0200 Subject: [PATCH 09/21] Aux window disrupts command palette (fix #195870) (#196231) --- src/vs/base/browser/dom.ts | 11 ++++++++- .../browser/quickInputController.ts | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 024e9988544..46290602d90 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -17,11 +17,15 @@ import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -export const { registerWindow, getWindows, onDidRegisterWindow } = (function () { +export const { registerWindow, getWindows, onDidRegisterWindow, onWillUnregisterWindow, onDidUnregisterWindow } = (function () { const windows = new Set([window]); const onDidRegisterWindow = new event.Emitter<{ window: Window & typeof globalThis; disposableStore: DisposableStore }>(); + const onDidUnregisterWindow = new event.Emitter(); + const onWillUnregisterWindow = new event.Emitter(); return { onDidRegisterWindow: onDidRegisterWindow.event, + onWillUnregisterWindow: onWillUnregisterWindow.event, + onDidUnregisterWindow: onDidUnregisterWindow.event, registerWindow(window: Window & typeof globalThis): IDisposable { if (windows.has(window)) { return Disposable.None; @@ -32,10 +36,15 @@ export const { registerWindow, getWindows, onDidRegisterWindow } = (function () const disposableStore = new DisposableStore(); disposableStore.add(toDisposable(() => { windows.delete(window); + onDidUnregisterWindow.fire(window); })); onDidRegisterWindow.fire({ window, disposableStore }); + disposableStore.add(addDisposableListener(window, 'beforeunload', () => { + onWillUnregisterWindow.fire(window); + })); + return disposableStore; }, getWindows(): Iterable { diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index acf5b64feb0..f0040a3206b 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -59,6 +59,15 @@ export class QuickInputController extends Disposable { this.parentElement = options.container; this.styles = options.styles; this._register(Event.runAndSubscribe(dom.onDidRegisterWindow, ({ window, disposableStore }) => this.registerKeyModsListeners(window, disposableStore), { window, disposableStore: this._store })); + this._register(dom.onWillUnregisterWindow(window => { + if (this.ui && dom.getWindow(this.ui.container) === window) { + // The window this quick input is contained in is about to + // close, so we have to make sure to reparent it back to an + // existing parent to not loose functionality. + // (https://github.com/microsoft/vscode/issues/195870) + this.reparentUI(this.layoutService.container); + } + })); } private registerKeyModsListeners(window: Window, disposables: DisposableStore): void { @@ -74,11 +83,10 @@ export class QuickInputController extends Disposable { private getUI() { if (this.ui) { - // In order to support aux windows, re-parent the controller if the original event is - // from a different document + // In order to support aux windows, re-parent the controller + // if the original event is from a different document if (this.parentElement.ownerDocument !== this.layoutService.activeContainer.ownerDocument) { - this.parentElement = this.layoutService.activeContainer; - dom.append(this.parentElement, this.ui.container); + this.reparentUI(this.layoutService.activeContainer); } return this.ui; @@ -306,6 +314,13 @@ export class QuickInputController extends Disposable { return this.ui; } + private reparentUI(container: HTMLElement): void { + if (this.ui) { + this.parentElement = container; + dom.append(this.parentElement, this.ui.container); + } + } + pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { type R = (O extends { canPickMany: true } ? T[] : T) | undefined; return new Promise((doResolve, reject) => { From 518d0f8f36b859a972516f6736514b9859dd78a6 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 23 Oct 2023 09:42:31 +0200 Subject: [PATCH 10/21] Rename menu --- .../workbench/browser/actions/layoutActions.ts | 17 +++++++---------- .../browser/parts/editor/editor.contribution.ts | 9 ++++----- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 853891af351..af6791a1ecf 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -489,7 +489,7 @@ export class HideEditorTabsAction extends Action2 { }); } - override run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); return configurationService.updateValue('workbench.editor.showTabs', 'none'); } @@ -515,7 +515,7 @@ export class ShowMultipleEditorTabsAction extends Action2 { }); } - override run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); return configurationService.updateValue('workbench.editor.showTabs', 'multiple'); } @@ -541,7 +541,7 @@ export class ShowSingleEditorTabAction extends Action2 { }); } - override run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); return configurationService.updateValue('workbench.editor.showTabs', 'single'); } @@ -550,13 +550,11 @@ registerAction2(ShowSingleEditorTabAction); // --- Toggle Pinned Tabs On Separate Row -export class ToggleSeparatePinnedTabsAction extends Action2 { - - static readonly ID = 'workbench.action.toggleSeparatePinnedEditorTabs'; +registerAction2(class extends Action2 { constructor() { super({ - id: ToggleSeparatePinnedTabsAction.ID, + id: 'workbench.action.toggleSeparatePinnedEditorTabs', title: { value: localize('toggleSeparatePinnedEditorTabs', "Separate Pinned Editor Tabs"), original: 'Separate Pinned Editor Tabs' @@ -567,7 +565,7 @@ export class ToggleSeparatePinnedTabsAction extends Action2 { }); } - override run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); const oldettingValue = configurationService.getValue('workbench.editor.pinnedTabsOnSeparateRow'); @@ -575,8 +573,7 @@ export class ToggleSeparatePinnedTabsAction extends Action2 { return configurationService.updateValue('workbench.editor.pinnedTabsOnSeparateRow', newSettingValue); } -} -registerAction2(ToggleSeparatePinnedTabsAction); +}); // --- Toggle Zen Mode diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1d76000dfcf..e0672ad76e2 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -10,7 +10,7 @@ import { IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/ed import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, - EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPinnedAndUnpinnedTabsContext + EditorTabsVisibleContext, ActiveEditorLastInGroupContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -64,7 +64,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; -import { HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction, ToggleSeparatePinnedTabsAction } from 'vs/workbench/browser/actions/layoutActions'; +import { HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions'; import product from 'vs/platform/product/common/product'; import { ICommandAction } from 'vs/platform/action/common/action'; @@ -354,11 +354,10 @@ MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: SPLIT_ MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: SPLIT_EDITOR_DOWN, title: localize('splitDown', "Split Down") }, group: '2_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: SPLIT_EDITOR_LEFT, title: localize('splitLeft', "Split Left") }, group: '2_split', order: 30 }); MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: SPLIT_EDITOR_RIGHT, title: localize('splitRight', "Split Right") }, group: '2_split', order: 40 }); -MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { submenu: MenuId.EditorTabsBarShowTabsSubmenu, title: localize('showTabs', "Show Tabs"), group: '3_config', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { submenu: MenuId.EditorTabsBarShowTabsSubmenu, title: localize('tabBar', "Tab bar"), group: '3_config', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTabsBarShowTabsSubmenu, { command: { id: ShowMultipleEditorTabsAction.ID, title: localize('multipleTabs', "Multiple Tabs"), toggled: ContextKeyExpr.equals('config.workbench.editor.showTabs', 'multiple') }, group: '1_config', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTabsBarShowTabsSubmenu, { command: { id: ShowSingleEditorTabAction.ID, title: localize('singleTab', "Single Tab"), toggled: ContextKeyExpr.equals('config.workbench.editor.showTabs', 'single') }, group: '1_config', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTabsBarShowTabsSubmenu, { command: { id: HideEditorTabsAction.ID, title: localize('hideTabBar', "Hide Tab Bar"), toggled: ContextKeyExpr.equals('config.workbench.editor.showTabs', 'none') }, group: '1_config', order: 30 }); -MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: ToggleSeparatePinnedTabsAction.ID, title: localize('toggleSeparatePinnedEditorTabs', "Separate Pinned Editor Tabs"), toggled: ContextKeyExpr.has('config.workbench.editor.pinnedTabsOnSeparateRow') }, when: EditorPinnedAndUnpinnedTabsContext, group: '3_config', order: 20 }); +MenuRegistry.appendMenuItem(MenuId.EditorTabsBarShowTabsSubmenu, { command: { id: HideEditorTabsAction.ID, title: localize('hideTabBar', "Hide"), toggled: ContextKeyExpr.equals('config.workbench.editor.showTabs', 'none') }, group: '1_config', order: 30 }); // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); From 5d9da91ba06dd9e8a91eb9e0cbb17b0c25c9dd3a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Oct 2023 11:49:49 +0200 Subject: [PATCH 11/21] editors - validate editor options (#196232) * editors - validate editor options * add more tests --- src/vs/base/common/objects.ts | 33 +++++- src/vs/base/common/types.ts | 12 ++ src/vs/base/test/common/objects.test.ts | 80 ++++++++++++++ .../workbench/browser/parts/editor/editor.ts | 104 ++++++++++++++++-- src/vs/workbench/common/editor.ts | 24 ++-- 5 files changed, 235 insertions(+), 18 deletions(-) diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 897a9fd8249..6fccb6f8589 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isTypedArray, isObject, isUndefinedOrNull } from 'vs/base/common/types'; +import { isTypedArray, isObject, isUndefinedOrNull, OptionalBooleanKey, OptionalNumberKey, OptionalStringKey } from 'vs/base/common/types'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -261,3 +261,34 @@ export function createProxyObject(methodNames: string[], invok } return result; } + +export function ensureOptionalBooleanValue(obj: T, key: OptionalBooleanKey, defaultValue: boolean | undefined): void { + if (typeof key !== 'string') { + return; + } + + if (obj[key] !== undefined && typeof obj[key] !== 'boolean') { + obj[key] = defaultValue as any; + } +} + +export function ensureOptionalNumberValue(obj: T, key: OptionalNumberKey, defaultValue: number | undefined): void { + if (typeof key !== 'string') { + return; + } + + if (obj[key] !== undefined && typeof obj[key] !== 'number') { + obj[key] = defaultValue as any; + } +} + +export function ensureOptionalStringValue(obj: T, key: OptionalStringKey, allowed: string[], defaultValue: string | undefined): void { + if (typeof key !== 'string') { + return; + } + + const value = obj[key]; + if (value !== undefined && (typeof value !== 'string' || !allowed.includes(value))) { + obj[key] = defaultValue as any; + } +} diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 529d5e4e7be..fc53cb11abb 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -227,3 +227,15 @@ export type Mutable = { * A single object or an array of the objects. */ export type SingleOrMany = T | T[]; + +export type OptionalBooleanKey = { + [K in keyof T]: T[K] extends boolean | undefined ? K : never; +}[keyof T]; + +export type OptionalNumberKey = { + [K in keyof T]: T[K] extends number | undefined ? K : never; +}[keyof T]; + +export type OptionalStringKey = { + [K in keyof T]: T[K] extends string | undefined ? K : never; +}[keyof T]; diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index 867615abce6..af797f47eea 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -227,4 +227,84 @@ suite('Objects', () => { assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'MIXEDCASE')); assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'mixedcase')); }); + + test('ensureOptionalBooleanValue', () => { + const obj: any = { + a: true, + b: false, + c: undefined, + d: 5, + e: 'foo' + }; + + objects.ensureOptionalBooleanValue(obj, 'a', false); + assert.strictEqual(obj.a, true); + + objects.ensureOptionalBooleanValue(obj, 'b', true); + assert.strictEqual(obj.b, false); + + objects.ensureOptionalBooleanValue(obj, 'c', true); + assert.strictEqual(obj.c, undefined); + + objects.ensureOptionalBooleanValue(obj, 'd', true); + assert.strictEqual(obj.d, true); + + objects.ensureOptionalBooleanValue(obj, 'e', true); + assert.strictEqual(obj.e, true); + }); + + test('ensureOptionalNumberValue', () => { + const obj: any = { + a: 1, + b: 0, + c: undefined, + d: true, + e: 'foo' + }; + + objects.ensureOptionalNumberValue(obj, 'a', 0); + assert.strictEqual(obj.a, 1); + + objects.ensureOptionalNumberValue(obj, 'b', 1); + assert.strictEqual(obj.b, 0); + + objects.ensureOptionalNumberValue(obj, 'c', 1); + assert.strictEqual(obj.c, undefined); + + objects.ensureOptionalNumberValue(obj, 'd', 1); + assert.strictEqual(obj.d, 1); + + objects.ensureOptionalNumberValue(obj, 'e', 1); + assert.strictEqual(obj.e, 1); + }); + + test('ensureOptionalStringValue', () => { + const obj: any = { + a: 'hello', + b: 'world', + c: undefined, + d: 'earth', + e: 5, + f: true + }; + + objects.ensureOptionalStringValue(obj, 'a', ['hello', 'world'], 'world'); + assert.strictEqual(obj.a, 'hello'); + + objects.ensureOptionalStringValue(obj, 'b', ['hello', 'world'], 'hello'); + assert.strictEqual(obj.b, 'world'); + + objects.ensureOptionalStringValue(obj, 'c', ['hello', 'world'], 'world'); + assert.strictEqual(obj.c, undefined); + + objects.ensureOptionalStringValue(obj, 'd', ['hello', 'world'], 'world'); + assert.strictEqual(obj.d, 'world'); + + objects.ensureOptionalStringValue(obj, 'e', ['hello', 'world'], 'world'); + assert.strictEqual(obj.e, 'world'); + + objects.ensureOptionalStringValue(obj, 'f', ['hello', 'world'], 'world'); + assert.strictEqual(obj.f, 'world'); + + }); }); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 6fecf20c309..425343b6cf3 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitConfiguration, IEditorPartDecorationsConfiguration } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -13,9 +13,10 @@ import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/co import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isObject } from 'vs/base/common/types'; +import { OptionalBooleanKey, OptionalNumberKey, OptionalStringKey, isObject } from 'vs/base/common/types'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; +import { ensureOptionalBooleanValue, ensureOptionalNumberValue, ensureOptionalStringValue } from 'vs/base/common/objects'; export interface IEditorPartCreationOptions { readonly restorePreviousState: boolean; @@ -90,13 +91,102 @@ export function getEditorPartOptions(configurationService: IConfigurationService return options; } -function validateEditorPartOptions(options: IEditorPartOptions) { - // showTabs ensure correct enum value +function validateEditorPartOptions(options: IEditorPartOptions): void { + + // Migrate: Show tabs (config migration kicks in very late and can cause flicker otherwise) if (typeof options.showTabs === 'boolean') { - // Migration service kicks in very late and can cause a flicker otherwise options.showTabs = options.showTabs ? 'multiple' : 'single'; - } else if (options.showTabs !== 'multiple' && options.showTabs !== 'single' && options.showTabs !== 'none') { - options.showTabs = 'multiple'; + } + + // Boolean options + const booleanOptions: Array> = [ + 'wrapTabs', + 'scrollToSwitchTabs', + 'highlightModifiedTabs', + 'pinnedTabsOnSeparateRow', + 'focusRecentEditorAfterClose', + 'showIcons', + 'enablePreview', + 'enablePreviewFromQuickOpen', + 'enablePreviewFromCodeNavigation', + 'closeOnFileDelete', + 'closeEmptyGroups', + 'revealIfOpen', + 'mouseBackForwardToNavigate', + 'restoreViewState', + 'splitOnDragAndDrop', + 'centeredLayoutFixedWidth', + 'doubleClickTabToToggleEditorGroupSizes' + ]; + for (const option of booleanOptions) { + if (typeof option === 'string') { + ensureOptionalBooleanValue(options, option, Boolean(DEFAULT_EDITOR_PART_OPTIONS[option])); + } + } + + // Number options + const numberOptions: Array> = [ + 'tabSizingFixedMinWidth', + 'tabSizingFixedMaxWidth' + ]; + for (const option of numberOptions) { + if (typeof option === 'string') { + ensureOptionalNumberValue(options, option, Number(DEFAULT_EDITOR_PART_OPTIONS[option])); + } + } + + // String options + const stringOptions: Array<[OptionalStringKey, Array]> = [ + ['showTabs', ['multiple', 'single', 'none']], + ['tabCloseButton', ['left', 'right', 'off']], + ['tabSizing', ['fit', 'shrink', 'fixed']], + ['pinnedTabSizing', ['normal', 'compact', 'shrink']], + ['tabHeight', ['default', 'compact']], + ['preventPinnedEditorClose', ['keyboardAndMouse', 'keyboard', 'mouse', 'never']], + ['titleScrollbarSizing', ['default', 'large']], + ['openPositioning', ['left', 'right', 'first', 'last']], + ['openSideBySideDirection', ['right', 'down']], + ['labelFormat', ['default', 'short', 'medium', 'long']], + ['splitInGroupLayout', ['vertical', 'horizontal']], + ['splitSizing', ['distribute', 'split', 'auto']], + ]; + for (const [option, allowed] of stringOptions) { + if (typeof option === 'string') { + ensureOptionalStringValue(options, option, allowed, String(DEFAULT_EDITOR_PART_OPTIONS[option])); + } + } + + // Complex options + if (options.autoLockGroups && !(options.autoLockGroups instanceof Set)) { + options.autoLockGroups = undefined; + } + if (options.limit && !isObject(options.limit)) { + options.limit = undefined; + } else if (options.limit) { + const booleanLimitOptions: Array> = [ + 'enabled', + 'excludeDirty', + 'perEditorGroup' + ]; + for (const option of booleanLimitOptions) { + if (typeof option === 'string') { + ensureOptionalBooleanValue(options.limit, option, undefined); + } + } + ensureOptionalNumberValue(options.limit, 'value', undefined); + } + if (options.decorations && !isObject(options.decorations)) { + options.decorations = undefined; + } else if (options.decorations) { + const booleanDecorationOptions: Array> = [ + 'badges', + 'colors' + ]; + for (const option of booleanDecorationOptions) { + if (typeof option === 'string') { + ensureOptionalBooleanValue(options.decorations, option, undefined); + } + } } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 36c62f1a362..60e2e472be2 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1095,6 +1095,18 @@ export interface IWorkbenchEditorConfiguration { }; } +export interface IEditorPartLimitConfiguration { + enabled?: boolean; + excludeDirty?: boolean; + value?: number; + perEditorGroup?: boolean; +} + +export interface IEditorPartDecorationsConfiguration { + badges?: boolean; + colors?: boolean; +} + interface IEditorPartConfiguration { showTabs?: 'multiple' | 'single' | 'none'; wrapTabs?: boolean; @@ -1128,16 +1140,8 @@ interface IEditorPartConfiguration { splitOnDragAndDrop?: boolean; centeredLayoutFixedWidth?: boolean; doubleClickTabToToggleEditorGroupSizes?: boolean; - limit?: { - enabled?: boolean; - excludeDirty?: boolean; - value?: number; - perEditorGroup?: boolean; - }; - decorations?: { - badges?: boolean; - colors?: boolean; - }; + limit?: IEditorPartLimitConfiguration; + decorations?: IEditorPartDecorationsConfiguration; } export interface IEditorPartOptions extends IEditorPartConfiguration { From bf9068c750e005f1762be0fdea8b924f426e2129 Mon Sep 17 00:00:00 2001 From: "ermin.zem" Date: Mon, 23 Oct 2023 18:14:58 +0800 Subject: [PATCH 12/21] fix: fix editor progress position when enabled pinnedTabsOnSeparateRow (#195314) * fix: fix editor progress position when enabled pinnedTabsOnSeparateRow * fix: fix editor progress position * fix: fix editor progress position * fix: add commit to editor title progress style * Revert "fix: add commit to editor title progress style" This reverts commit 302ec88c9ad48074669b5abf97fd294549bceb24. * Revert "fix: fix editor progress position" This reverts commit ec445b607cd0b64febe84b14b3cb261ea32f9c0c. * Revert "fix: fix editor progress position" This reverts commit cea64becfa492826069486462df5287dafb78aa2. * Revert "fix: fix editor progress position when enabled pinnedTabsOnSeparateRow" This reverts commit 8f2c0e702205dbd04599605b86db5f872d61473a. * feat: add --editor-group-tabs-height css variable to set progress bar position * update height in lyout * Remove setting redraws as already done elsewhere * Remove 2px for progress bit height --------- Co-authored-by: ermin.zem Co-authored-by: BeniBenj --- build/lib/stylelint/vscode-known-variables.json | 3 ++- src/vs/workbench/browser/parts/editor/editorGroupView.ts | 2 ++ .../workbench/browser/parts/editor/media/editorgroupview.css | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 70475768de0..5f33f35fc19 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -737,6 +737,7 @@ "--tab-sizing-fixed-min-width", "--tab-sizing-fixed-max-width", "--editor-group-tab-height", + "--editor-group-tabs-height", "--testMessageDecorationFontFamily", "--testMessageDecorationFontSize", "--title-border-bottom-color", @@ -783,4 +784,4 @@ "--z-index-notebook-sticky-scroll", "--zoom-factor" ] -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 32b7296069f..b898596878f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1930,6 +1930,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { available: new Dimension(width, height - this.editorPane.minimumHeight) }); + this.element.style.setProperty('--editor-group-tabs-height', `${this.titleHeight.offset}px`); + // Pass the container width and remaining height to the editor layout const editorHeight = Math.max(0, height - titleControlSize.height); this.editorContainer.style.height = `${editorHeight}px`; diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 676c6e0fff8..f61bb8aaa45 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -185,3 +185,8 @@ width: 100%; height: 100%; } + +/* Override top position of progress bar which defined in vs/workbench/browser/media/part.css */ +.monaco-workbench .part.editor > .content .monaco-progress-container { + top: min(calc(var(--editor-group-tabs-height) - 2px), 0); +} From 246abc0ecbb7089c5c873db09f5940dbfd8e8b31 Mon Sep 17 00:00:00 2001 From: Benjamin Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:37:17 +0200 Subject: [PATCH 13/21] Maximize editor group (#194894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Maximize editor * unmaximze instead of exit maximize * add tests * :lipstick: * adopt changes * :lipstick: * :lipstick: * :lipstick: * :lipstick: * :lipstick: * :lipstick: * move context key to WorkbenchContextKeysHandler * :lipstick: * :lipstick: * bug fix * :lipstick: * touch file to remove from diff * test maximized group event * Update editor group behavior * :lipstick: * :lipstick: * :lipstick: * :lipstick: * :lipstick: * :lipstick: * setvisibility unmaximizes view if maximized * Add visibility option to serialized branch node and update gridview reconstruction behavior * Fix arrangeGroups method signature * Save maximized view by location * Fix activeGroup check in EditorPart * :lipstick: * :lipstick: * Add gridWidgetDisposables to prevent memory leaks. * simplify GridView.setChildVisible * Refactor Grid class to return IDisposable from maximizeView method * :lipstick: * move maximizing to gridview layer * Fix trySet2x2 method to exit maximized view before setting 2x2 * fix bug * hasMaximizedGroup * Refactor GridView for hasMaximizedView * fix migrations * Fix double click tab toggle value in workbench editor * :lipstick: * Fix editor group expansion test * :lipstick: * :lipstick: * :lipstick: * Fix migration with new settings validation --------- Co-authored-by: Benjamin Pasero Co-authored-by: João Moreno --- src/vs/base/browser/ui/grid/grid.ts | 65 +++++- src/vs/base/browser/ui/grid/gridview.ts | 165 +++++++++++++- src/vs/base/browser/ui/splitview/splitview.ts | 27 ++- src/vs/base/test/browser/ui/grid/grid.test.ts | 202 +++++++++++++++++- .../test/browser/ui/grid/gridview.test.ts | 2 +- src/vs/workbench/browser/contextkeys.ts | 6 +- .../parts/editor/editor.contribution.ts | 13 +- .../workbench/browser/parts/editor/editor.ts | 6 +- .../browser/parts/editor/editorActions.ts | 71 +++++- .../browser/parts/editor/editorCommands.ts | 10 +- .../browser/parts/editor/editorPart.ts | 77 +++++-- .../browser/parts/editor/editorParts.ts | 26 ++- .../parts/editor/multiEditorTabsControl.ts | 14 +- .../browser/workbench.contribution.ts | 24 ++- src/vs/workbench/common/contextkeys.ts | 1 + src/vs/workbench/common/editor.ts | 2 +- .../editor/common/editorGroupsService.ts | 32 ++- .../test/browser/editorGroupsService.test.ts | 41 +++- .../editor/test/browser/editorService.test.ts | 8 +- .../test/browser/workbenchTestServices.ts | 5 + 20 files changed, 700 insertions(+), 97 deletions(-) diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 821567ccf59..4f772b198c2 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -51,6 +51,7 @@ export interface GridLeafNode { readonly view: T; readonly box: Box; readonly cachedVisibleSize: number | undefined; + readonly maximized: boolean; } export interface GridBranchNode { @@ -288,6 +289,7 @@ export class Grid extends Disposable { private didLayout = false; + readonly onDidChangeViewMaximized: Event; /** * Create a new {@link Grid}. A grid must *always* have a view * inside. @@ -313,6 +315,7 @@ export class Grid extends Disposable { this.onDidChange = this.gridview.onDidChange; this.onDidScroll = this.gridview.onDidScroll; + this.onDidChangeViewMaximized = this.gridview.onDidChangeViewMaximized; } style(styles: IGridStyles): void { @@ -545,9 +548,28 @@ export class Grid extends Disposable { * * @param view The reference {@link IView view}. */ - isViewSizeMaximized(view: T): boolean { + isViewExpanded(view: T): boolean { const location = this.getViewLocation(view); - return this.gridview.isViewSizeMaximized(location); + return this.gridview.isViewExpanded(location); + } + + /** + * Returns whether the {@link IView view} is maximized. + * + * @param view The reference {@link IView view}. + */ + isViewMaximized(view: T): boolean { + const location = this.getViewLocation(view); + return this.gridview.isViewMaximized(location); + } + + /** + * Returns whether the {@link IView view} is maximized. + * + * @param view The reference {@link IView view}. + */ + hasMaximizedView(): boolean { + return this.gridview.hasMaximizedView(); } /** @@ -577,14 +599,30 @@ export class Grid extends Disposable { } /** - * Maximize the size of a {@link IView view} by collapsing all other views + * Maximizes the specified view and hides all other views. + * @param view The view to maximize. + */ + maximizeView(view: T) { + if (this.views.size < 2) { + throw new Error('At least two views are required to maximize a view'); + } + const location = this.getViewLocation(view); + this.gridview.maximizeView(location); + } + + exitMaximizedView(): void { + this.gridview.exitMaximizedView(); + } + + /** + * Expand the size of a {@link IView view} by collapsing all other views * to their minimum sizes. * * @param view The {@link IView view}. */ - maximizeViewSize(view: T): void { + expandView(view: T): void { const location = this.getViewLocation(view); - this.gridview.maximizeViewSize(location); + this.gridview.expandView(location); } /** @@ -713,12 +751,14 @@ export interface ISerializedLeafNode { data: any; size: number; visible?: boolean; + maximized?: boolean; } export interface ISerializedBranchNode { type: 'branch'; data: ISerializedNode[]; size: number; + visible?: boolean; } export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode; @@ -739,14 +779,23 @@ export class SerializableGrid extends Grid { const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height; if (!isGridBranchNode(node)) { + const serializedLeafNode: ISerializedLeafNode = { type: 'leaf', data: node.view.toJSON(), size }; + if (typeof node.cachedVisibleSize === 'number') { - return { type: 'leaf', data: node.view.toJSON(), size: node.cachedVisibleSize, visible: false }; + serializedLeafNode.size = node.cachedVisibleSize; + serializedLeafNode.visible = false; + } else if (node.maximized) { + serializedLeafNode.maximized = true; } - return { type: 'leaf', data: node.view.toJSON(), size }; + return serializedLeafNode; } - return { type: 'branch', data: node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation))), size }; + const data = node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation))); + if (data.some(c => c.visible !== false)) { + return { type: 'branch', data: data, size }; + } + return { type: 'branch', data: data, size, visible: false }; } /** diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index f73e7a8ce7a..e21d06a9936 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -148,12 +148,14 @@ export interface ISerializedLeafNode { data: any; size: number; visible?: boolean; + maximized?: boolean; } export interface ISerializedBranchNode { type: 'branch'; data: ISerializedNode[]; size: number; + visible?: boolean; } export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode; @@ -180,6 +182,7 @@ export interface GridLeafNode { readonly view: IView; readonly box: Box; readonly cachedVisibleSize: number | undefined; + readonly maximized: boolean; } export interface GridBranchNode { @@ -284,11 +287,11 @@ class BranchNode implements ISplitView, IDisposable { } get minimumSize(): number { - return this.children.length === 0 ? 0 : Math.max(...this.children.map(c => c.minimumOrthogonalSize)); + return this.children.length === 0 ? 0 : Math.max(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.minimumOrthogonalSize : 0)); } get maximumSize(): number { - return Math.min(...this.children.map(c => c.maximumOrthogonalSize)); + return Math.min(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.maximumOrthogonalSize : Number.POSITIVE_INFINITY)); } get priority(): LayoutPriority { @@ -342,6 +345,10 @@ class BranchNode implements ISplitView, IDisposable { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidVisibilityChange = new Emitter(); + readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; + private readonly childrenVisibilityChangeDisposable: DisposableStore = new DisposableStore(); + private _onDidScroll = new Emitter(); private onDidScrollDisposable: IDisposable = Disposable.None; readonly onDidScroll: Event = this._onDidScroll.event; @@ -427,7 +434,7 @@ class BranchNode implements ISplitView, IDisposable { return { view: childDescriptor.node, size: childDescriptor.node.size, - visible: childDescriptor.node instanceof LeafNode && childDescriptor.visible !== undefined ? childDescriptor.visible : true + visible: childDescriptor.visible !== false }; }), size: this.orthogonalSize @@ -579,8 +586,8 @@ class BranchNode implements ISplitView, IDisposable { this.splitview.resizeView(index, size); } - isChildSizeMaximized(index: number): boolean { - return this.splitview.isViewSizeMaximized(index); + isChildExpanded(index: number): boolean { + return this.splitview.isViewExpanded(index); } distributeViewSizes(recursive = false): void { @@ -614,7 +621,15 @@ class BranchNode implements ISplitView, IDisposable { return; } + const wereAllChildrenHidden = this.splitview.contentSize === 0; this.splitview.setViewVisible(index, visible); + const areAllChildrenHidden = this.splitview.contentSize === 0; + + // If all children are hidden then the parent should hide the entire splitview + // If the entire splitview is hidden then the parent should show the splitview when a child is shown + if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) { + this._onDidVisibilityChange.fire(visible); + } } getChildCachedVisibleSize(index: number): number | undefined { @@ -651,6 +666,15 @@ class BranchNode implements ISplitView, IDisposable { const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll)); this.onDidScrollDisposable.dispose(); this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll); + + this.childrenVisibilityChangeDisposable.clear(); + this.children.forEach((child, index) => { + if (child instanceof BranchNode) { + this.childrenVisibilityChangeDisposable.add(child.onDidVisibilityChange((visible) => { + this.setChildVisible(index, visible); + })); + } + }); } trySet2x2(other: BranchNode): IDisposable { @@ -714,7 +738,9 @@ class BranchNode implements ISplitView, IDisposable { this._onDidChange.dispose(); this._onDidSashReset.dispose(); + this._onDidVisibilityChange.dispose(); + this.childrenVisibilityChangeDisposable.dispose(); this.splitviewSashResetDisposable.dispose(); this.childrenSashResetDisposable.dispose(); this.childrenChangeDisposable.dispose(); @@ -1128,6 +1154,11 @@ export class GridView implements IDisposable { this.root.edgeSnapping = edgeSnapping; } + private maximizedNode: LeafNode | undefined = undefined; + + private readonly _onDidChangeViewMaximized = new Emitter(); + readonly onDidChangeViewMaximized = this._onDidChangeViewMaximized.event; + /** * Create a new {@link GridView} instance. * @@ -1173,6 +1204,10 @@ export class GridView implements IDisposable { * @param location The {@link GridLocation location} to insert the view on. */ addView(view: IView, size: number | Sizing, location: GridLocation): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + this.disposable2x2.dispose(); this.disposable2x2 = Disposable.None; @@ -1226,6 +1261,10 @@ export class GridView implements IDisposable { * @param sizing Whether to distribute other {@link IView view}'s sizes. */ removeView(location: GridLocation, sizing?: DistributeSizing | AutoSizing): IView { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + this.disposable2x2.dispose(); this.disposable2x2 = Disposable.None; @@ -1312,6 +1351,10 @@ export class GridView implements IDisposable { * @param to The index where the {@link IView view} should move to. */ moveView(parentLocation: GridLocation, from: number, to: number): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [, parent] = this.getNode(parentLocation); if (!(parent instanceof BranchNode)) { @@ -1330,6 +1373,10 @@ export class GridView implements IDisposable { * @param to The {@link GridLocation location} of another view. */ swapViews(from: GridLocation, to: GridLocation): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [fromRest, fromIndex] = tail(from); const [, fromParent] = this.getNode(fromRest); @@ -1378,6 +1425,10 @@ export class GridView implements IDisposable { * @param size The size the view should be. Optionally provide a single dimension. */ resizeView(location: GridLocation, size: Partial): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); @@ -1443,7 +1494,11 @@ export class GridView implements IDisposable { * * @param location The {@link GridLocation location} of the view. */ - maximizeViewSize(location: GridLocation): void { + expandView(location: GridLocation): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [ancestors, node] = this.getNode(location); if (!(node instanceof LeafNode)) { @@ -1460,7 +1515,12 @@ export class GridView implements IDisposable { * * @param location The {@link GridLocation location} of the view. */ - isViewSizeMaximized(location: GridLocation): boolean { + isViewExpanded(location: GridLocation): boolean { + if (this.hasMaximizedView()) { + // No view can be expanded when a view is maximized + return false; + } + const [ancestors, node] = this.getNode(location); if (!(node instanceof LeafNode)) { @@ -1468,7 +1528,7 @@ export class GridView implements IDisposable { } for (let i = 0; i < ancestors.length; i++) { - if (!ancestors[i].isChildSizeMaximized(location[i])) { + if (!ancestors[i].isChildExpanded(location[i])) { return false; } } @@ -1476,6 +1536,80 @@ export class GridView implements IDisposable { return true; } + maximizeView(location: GridLocation) { + const [, nodeToMaximize] = this.getNode(location); + if (!(nodeToMaximize instanceof LeafNode)) { + throw new Error('Location is not a LeafNode'); + } + + if (this.maximizedNode === nodeToMaximize) { + return; + } + + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + + function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + if (child instanceof LeafNode) { + if (child !== exclude) { + parent.setChildVisible(i, false); + } + } else { + hideAllViewsBut(child, exclude); + } + } + } + + hideAllViewsBut(this.root, nodeToMaximize); + + this.maximizedNode = nodeToMaximize; + this._onDidChangeViewMaximized.fire(true); + } + + exitMaximizedView(): void { + if (!this.maximizedNode) { + return; + } + this.maximizedNode = undefined; + + // When hiding a view, it's previous size is cached. + // To restore the sizes of all views, they need to be made visible in reverse order. + function showViewsInReverseOrder(parent: BranchNode): void { + for (let index = parent.children.length - 1; index >= 0; index--) { + const child = parent.children[index]; + if (child instanceof LeafNode) { + parent.setChildVisible(index, true); + } else { + showViewsInReverseOrder(child); + } + } + } + + showViewsInReverseOrder(this.root); + + this._onDidChangeViewMaximized.fire(false); + } + + hasMaximizedView(): boolean { + return this.maximizedNode !== undefined; + } + + /** + * Returns whether the {@link IView view} is maximized. + * + * @param location The {@link GridLocation location} of the view. + */ + isViewMaximized(location: GridLocation): boolean { + const [, node] = this.getNode(location); + if (!(node instanceof LeafNode)) { + throw new Error('Location is not a LeafNode'); + } + return node === this.maximizedNode; + } + /** * Distribute the size among all {@link IView views} within the entire * grid or within a single {@link SplitView}. @@ -1486,6 +1620,10 @@ export class GridView implements IDisposable { * in the entire grid. */ distributeViewSizes(location?: GridLocation): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + if (!location) { this.root.distributeViewSizes(true); return; @@ -1523,6 +1661,11 @@ export class GridView implements IDisposable { * @param location The {@link GridLocation location} of the view. */ setViewVisible(location: GridLocation, visible: boolean): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + return; + } + const [rest, index] = tail(location); const [, parent] = this.getNode(rest); @@ -1596,6 +1739,10 @@ export class GridView implements IDisposable { result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children); } else { result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size); + if (node.maximized && !this.maximizedNode) { + this.maximizedNode = result; + this._onDidChangeViewMaximized.fire(true); + } } return result; @@ -1605,7 +1752,7 @@ export class GridView implements IDisposable { const box = { top: node.top, left: node.left, width: node.width, height: node.height }; if (node instanceof LeafNode) { - return { view: node.view, box, cachedVisibleSize }; + return { view: node.view, box, cachedVisibleSize, maximized: this.maximizedNode === node }; } const children: GridNode[] = []; diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index ca8a714d8b3..8f63b6039a1 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -442,7 +442,7 @@ export class SplitView[] = []; sashItems: ISashItem[] = []; // used in tests @@ -459,6 +459,11 @@ export class SplitView r + i.size, 0); + this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.saveProportions(); } } @@ -834,7 +839,7 @@ export class SplitView 0) { item.size = clamp(Math.round(proportion * size / total), item.minimumSize, item.maximumSize); } } @@ -873,8 +878,8 @@ export class SplitView 0) { - this.proportions = this.viewItems.map(i => i.proportionalLayout ? i.size / this.contentSize : undefined); + if (this.proportionalLayout && this._contentSize > 0) { + this.proportions = this.viewItems.map(v => v.proportionalLayout && v.visible ? v.size / this._contentSize : undefined); } } @@ -1052,7 +1057,7 @@ export class SplitView= this.viewItems.length) { return false; } @@ -1347,7 +1352,7 @@ export class SplitView r + i.size, 0); + this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); // Layout views let offset = 0; @@ -1367,12 +1372,12 @@ export class SplitView 0 || this.startSnappingEnabled)) { sash.state = SashState.AtMinimum; - } else if (snappedAfter && collapsesDown[index] && (position < this.contentSize || this.endSnappingEnabled)) { + } else if (snappedAfter && collapsesDown[index] && (position < this._contentSize || this.endSnappingEnabled)) { sash.state = SashState.AtMaximum; } else { sash.state = SashState.Disabled; diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index b4e5e4d6c16..781a0a407f0 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { createSerializedGrid, Direction, getRelativeLocation, Grid, GridNode, GridNodeDescriptor, ISerializableView, isGridBranchNode, IViewDeserializer, Orientation, sanitizeGridNodeDescriptor, SerializableGrid, Sizing } from 'vs/base/browser/ui/grid/grid'; import { Event } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; -import { nodesToArrays, TestView } from './util'; +import { nodesToArrays, TestView } from 'vs/base/test/browser/ui/grid/util'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -464,6 +464,206 @@ suite('Grid', function () { assert.deepStrictEqual(grid.getNeighborViews(view1, Direction.Right), [view2, view3]); }); + + test('hiding splitviews and restoring sizes', function () { + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); + container.appendChild(grid.element); + + grid.layout(800, 600); + + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view2, Sizing.Distribute, view1, Direction.Right); + + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view3, Sizing.Distribute, view2, Direction.Down); + + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view4, Sizing.Distribute, view2, Direction.Right); + + const size1 = view1.size; + const size2 = view2.size; + const size3 = view3.size; + const size4 = view4.size; + + grid.maximizeView(view1); + + // Views 2, 3, 4 are hidden + // Splitview (2,4) and ((2,4),3) are hidden + assert.deepStrictEqual(view1.size, [800, 600]); + assert.deepStrictEqual(view2.size, [0, 0]); + assert.deepStrictEqual(view3.size, [0, 0]); + assert.deepStrictEqual(view4.size, [0, 0]); + + grid.exitMaximizedView(); + + assert.deepStrictEqual(view1.size, size1); + assert.deepStrictEqual(view2.size, size2); + assert.deepStrictEqual(view3.size, size3); + assert.deepStrictEqual(view4.size, size4); + + // Views 1, 3, 4 are hidden + // All splitviews are still visible => only orthogonalsize is 0 + grid.maximizeView(view2); + + assert.deepStrictEqual(view1.size, [0, 600]); + assert.deepStrictEqual(view2.size, [800, 600]); + assert.deepStrictEqual(view3.size, [800, 0]); + assert.deepStrictEqual(view4.size, [0, 600]); + + grid.exitMaximizedView(); + + assert.deepStrictEqual(view1.size, size1); + assert.deepStrictEqual(view2.size, size2); + assert.deepStrictEqual(view3.size, size3); + assert.deepStrictEqual(view4.size, size4); + }); + + test('hasMaximizedView', function () { + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); + container.appendChild(grid.element); + + grid.layout(800, 600); + + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view2, Sizing.Distribute, view1, Direction.Right); + + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view3, Sizing.Distribute, view2, Direction.Down); + + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view4, Sizing.Distribute, view2, Direction.Right); + + function checkIsMaximized(view: TestView) { + grid.maximizeView(view); + + assert.deepStrictEqual(grid.hasMaximizedView(), true); + + // When a view is maximized, no view can be expanded even if it is maximized + assert.deepStrictEqual(grid.isViewExpanded(view1), false); + assert.deepStrictEqual(grid.isViewExpanded(view2), false); + assert.deepStrictEqual(grid.isViewExpanded(view3), false); + assert.deepStrictEqual(grid.isViewExpanded(view4), false); + + grid.exitMaximizedView(); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + } + + checkIsMaximized(view1); + checkIsMaximized(view2); + checkIsMaximized(view3); + checkIsMaximized(view4); + }); + + test('Changes to the grid unmaximize the view', function () { + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); + container.appendChild(grid.element); + + grid.layout(800, 600); + + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view2, Sizing.Distribute, view1, Direction.Right); + + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view3, Sizing.Distribute, view2, Direction.Down); + + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + + // Adding a view unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.addView(view4, Sizing.Distribute, view2, Direction.Right); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + assert.deepStrictEqual(grid.isViewVisible(view4), true); + + // Removing a view unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.removeView(view4); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + + // Changing the visibility of any view while a view is maximized, unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.setViewVisible(view3, true); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + }); + + test('Changes to the grid sizing unmaximize the view', function () { + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); + container.appendChild(grid.element); + + grid.layout(800, 600); + + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view2, Sizing.Distribute, view1, Direction.Right); + + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view3, Sizing.Distribute, view2, Direction.Down); + + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + grid.addView(view4, Sizing.Distribute, view2, Direction.Right); + + // Maximizing a different view unmaximizes the current one and maximizes the new one + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.maximizeView(view2); + + assert.deepStrictEqual(grid.hasMaximizedView(), true); + assert.deepStrictEqual(grid.isViewVisible(view1), false); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), false); + assert.deepStrictEqual(grid.isViewVisible(view4), false); + + // Distributing the size unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.distributeViewSizes(); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + assert.deepStrictEqual(grid.isViewVisible(view4), true); + + // Expanding a different view unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.expandView(view2); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + assert.deepStrictEqual(grid.isViewVisible(view4), true); + + // Expanding the maximized view unmaximizes the view + grid.maximizeView(view1); + assert.deepStrictEqual(grid.hasMaximizedView(), true); + grid.expandView(view1); + + assert.deepStrictEqual(grid.hasMaximizedView(), false); + assert.deepStrictEqual(grid.isViewVisible(view1), true); + assert.deepStrictEqual(grid.isViewVisible(view2), true); + assert.deepStrictEqual(grid.isViewVisible(view3), true); + assert.deepStrictEqual(grid.isViewVisible(view4), true); + }); }); class TestSerializableView extends TestView implements ISerializableView { diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts index e9be1b00e16..bbc7de0716f 100644 --- a/src/vs/base/test/browser/ui/grid/gridview.test.ts +++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { GridView, IView, Orientation, Sizing } from 'vs/base/browser/ui/grid/gridview'; -import { nodesToArrays, TestView } from './util'; +import { nodesToArrays, TestView } from 'vs/base/test/browser/ui/grid/util'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Gridview', function () { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index fcddfd142d8..f052cd5f7a9 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext } from 'vs/workbench/common/contextkeys'; +import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, MaximizedEditorGroupContext, TitleBarVisibleContext } from 'vs/workbench/common/contextkeys'; import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -44,6 +44,7 @@ export class WorkbenchContextKeysHandler extends Disposable { private activeEditorGroupLast: IContextKey; private activeEditorGroupLocked: IContextKey; private multipleEditorGroupsContext: IContextKey; + private maximizedEditorGroupContext: IContextKey; private editorsVisibleContext: IContextKey; @@ -136,6 +137,7 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); this.activeEditorGroupLocked = ActiveEditorGroupLockedContext.bindTo(this.contextKeyService); this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + this.maximizedEditorGroupContext = MaximizedEditorGroupContext.bindTo(this.contextKeyService); // Working Copies this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); @@ -233,6 +235,8 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupContextKeys())); this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupMaximized((maximized) => this.maximizedEditorGroupContext.set(maximized))); + this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposableStore }) => disposableStore.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document), true)), { window, disposableStore: this._store })); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index e0672ad76e2..43df6bf2818 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -10,7 +10,7 @@ import { IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/ed import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, - EditorTabsVisibleContext, ActiveEditorLastInGroupContext + EditorTabsVisibleContext, ActiveEditorLastInGroupContext, MaximizedEditorGroupContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -40,13 +40,14 @@ import { QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReOpenInTextEditorAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction, ToggleEditorTypeAction, SplitEditorToAboveGroupAction, SplitEditorToBelowGroupAction, SplitEditorToFirstGroupAction, SplitEditorToLastGroupAction, SplitEditorToLeftGroupAction, SplitEditorToNextGroupAction, SplitEditorToPreviousGroupAction, SplitEditorToRightGroupAction, NavigateForwardInEditsAction, - NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction, ExperimentalMoveEditorIntoNewWindowAction + NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction, + MaximizeGroupHideSidebarAction, UnmaximizeEditorGroupAction, ExperimentalMoveEditorIntoNewWindowAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands, REOPEN_WITH_COMMAND_ID, - TOGGLE_LOCK_GROUP_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID, SPLIT_EDITOR_IN_GROUP, JOIN_EDITOR_IN_GROUP, FOCUS_FIRST_SIDE_EDITOR, FOCUS_SECOND_SIDE_EDITOR, TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT, SPLIT_EDITOR + TOGGLE_LOCK_GROUP_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID, SPLIT_EDITOR_IN_GROUP, JOIN_EDITOR_IN_GROUP, FOCUS_FIRST_SIDE_EDITOR, FOCUS_SECOND_SIDE_EDITOR, TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT, SPLIT_EDITOR, MAXIMIZE_EDITOR_GROUP, UNMAXIMIZE_EDITOR_GROUP } from 'vs/workbench/browser/parts/editor/editorCommands'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -214,6 +215,8 @@ registerAction2(NavigateBetweenGroupsAction); registerAction2(ResetGroupSizesAction); registerAction2(ToggleGroupSizesAction); registerAction2(MaximizeGroupAction); +registerAction2(UnmaximizeEditorGroupAction); +registerAction2(MaximizeGroupHideSidebarAction); registerAction2(MinimizeOtherGroupsAction); registerAction2(MoveEditorLeftInGroupAction); @@ -382,7 +385,9 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: SHOW_EDITORS_IN MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '5_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_KEEP_EDITORS_COMMAND_ID, title: localize('togglePreviewMode', "Enable Preview Editors"), toggled: ContextKeyExpr.has('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_lock', order: 10, when: MultipleEditorGroupsContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: MAXIMIZE_EDITOR_GROUP, title: localize('maximizeGroup', "Maximize Group") }, group: '8_group_operations', order: 5, when: ContextKeyExpr.and(MaximizedEditorGroupContext.negate(), MultipleEditorGroupsContext) }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: UNMAXIMIZE_EDITOR_GROUP, title: localize('unmaximizeGroup', "Unmaximize Group") }, group: '8_group_operations', order: 5, when: MaximizedEditorGroupContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_group_operations', order: 10, when: MultipleEditorGroupsContext }); function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpression | undefined, order: number, alternative?: ICommandAction, precondition?: ContextKeyExpression | undefined): void { const item: IMenuItem = { diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 425343b6cf3..0ad637398b2 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -48,7 +48,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { splitSizing: 'auto', splitOnDragAndDrop: true, centeredLayoutFixedWidth: false, - doubleClickTabToToggleEditorGroupSizes: true, + doubleClickTabToToggleEditorGroupSizes: 'expand', }; export function impactsEditorPartOptions(event: IConfigurationChangeEvent): boolean { @@ -116,7 +116,6 @@ function validateEditorPartOptions(options: IEditorPartOptions): void { 'restoreViewState', 'splitOnDragAndDrop', 'centeredLayoutFixedWidth', - 'doubleClickTabToToggleEditorGroupSizes' ]; for (const option of booleanOptions) { if (typeof option === 'string') { @@ -149,6 +148,7 @@ function validateEditorPartOptions(options: IEditorPartOptions): void { ['labelFormat', ['default', 'short', 'medium', 'long']], ['splitInGroupLayout', ['vertical', 'horizontal']], ['splitSizing', ['distribute', 'split', 'auto']], + ['doubleClickTabToToggleEditorGroupSizes', ['maximize', 'expand', 'off']] ]; for (const [option, allowed] of stringOptions) { if (typeof option === 'string') { @@ -235,6 +235,8 @@ export interface IEditorGroupsView { removeGroup(group: IEditorGroupView | GroupIdentifier): void; arrangeGroups(arrangement: GroupsArrangement, target?: IEditorGroupView | GroupIdentifier): void; + toggleMaximizeGroup(group?: IEditorGroupView | GroupIdentifier): void; + toggleExpandGroup(group?: IEditorGroupView | GroupIdentifier): void; } export interface IEditorGroupTitleHeight { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 86e2ec7c78a..38800f952db 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, UNMAXIMIZE_EDITOR_GROUP, MAXIMIZE_EDITOR_GROUP, resolveCommandsContext, getCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -33,7 +33,8 @@ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext, MaximizedEditorGroupContext, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; +import { URI } from 'vs/base/common/uri'; import { getActiveDocument } from 'vs/base/browser/dom'; class ExecuteCommandAction extends Action2 { @@ -1014,7 +1015,7 @@ export class MinimizeOtherGroupsAction extends Action2 { constructor() { super({ id: 'workbench.action.minimizeOtherEditors', - title: { value: localize('minimizeOtherEditorGroups', "Maximize Editor Group"), original: 'Maximize Editor Group' }, + title: { value: localize('minimizeOtherEditorGroups', "Expand Editor Group"), original: 'Expand Editor Group' }, f1: true, category: Categories.View }); @@ -1023,7 +1024,7 @@ export class MinimizeOtherGroupsAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - editorGroupService.arrangeGroups(GroupsArrangement.MAXIMIZE); + editorGroupService.arrangeGroups(GroupsArrangement.EXPAND); } } @@ -1059,7 +1060,7 @@ export class ToggleGroupSizesAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - editorGroupService.arrangeGroups(GroupsArrangement.TOGGLE); + editorGroupService.toggleExpandGroup(); } } @@ -1067,10 +1068,34 @@ export class MaximizeGroupAction extends Action2 { constructor() { super({ - id: 'workbench.action.maximizeEditor', - title: { value: localize('maximizeEditor', "Maximize Editor Group and Hide Side Bars"), original: 'Maximize Editor Group and Hide Side Bars' }, + id: MAXIMIZE_EDITOR_GROUP, + title: { value: localize('maximizeEditor', "Maximize Editor Group"), original: 'Maximize Editor Group' }, f1: true, - category: Categories.View + category: Categories.View, + precondition: ContextKeyExpr.and(MaximizedEditorGroupContext.negate(), MultipleEditorGroupsContext), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyM), + } + }); + } + + override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + const editorsGroupService = accessor.get(IEditorGroupsService); + const { group } = resolveCommandsContext(editorsGroupService, getCommandsContext(resourceOrContext, context)); + editorsGroupService.arrangeGroups(GroupsArrangement.MAXIMIZE, group); + } +} + +export class MaximizeGroupHideSidebarAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.maximizeEditorHideSidebar', + title: { value: localize('maximizeEditorHideSidebar', "Maximize Editor Group and Hide Side Bars"), original: 'Maximize Editor Group and Hide Side Bars' }, + f1: true, + category: Categories.View, + precondition: ContextKeyExpr.and(MaximizedEditorGroupContext.negate(), MultipleEditorGroupsContext) }); } @@ -1087,6 +1112,36 @@ export class MaximizeGroupAction extends Action2 { } } +export class UnmaximizeEditorGroupAction extends Action2 { + + constructor() { + super({ + id: UNMAXIMIZE_EDITOR_GROUP, + title: { value: localize('UnmaximizeEditorGroup', "Unmaximize Editor Group"), original: 'Unmaximize Editor Group' }, + f1: true, + category: Categories.View, + precondition: MaximizedEditorGroupContext, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyM), + }, + menu: { + id: MenuId.EditorTitle, + order: -10000, // towards the front + group: 'navigation', + when: MaximizedEditorGroupContext + }, + icon: Codicon.screenFull, + toggled: MaximizedEditorGroupContext, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorGroupService = accessor.get(IEditorGroupsService); + editorGroupService.toggleMaximizeGroup(); + } +} + abstract class AbstractNavigateEditorAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 44e916e0629..a6e013ebf4f 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -80,6 +80,9 @@ export const SPLIT_EDITOR_DOWN = 'workbench.action.splitEditorDown'; export const SPLIT_EDITOR_LEFT = 'workbench.action.splitEditorLeft'; export const SPLIT_EDITOR_RIGHT = 'workbench.action.splitEditorRight'; +export const MAXIMIZE_EDITOR_GROUP = 'workbench.action.maximizeEditorGroup'; +export const UNMAXIMIZE_EDITOR_GROUP = 'workbench.action.unmaximizeEditorGroup'; + export const SPLIT_EDITOR_IN_GROUP = 'workbench.action.splitEditorInGroup'; export const TOGGLE_SPLIT_EDITOR_IN_GROUP = 'workbench.action.toggleSplitEditorInGroup'; export const JOIN_EDITOR_IN_GROUP = 'workbench.action.joinEditorInGroup'; @@ -104,7 +107,8 @@ export const EDITOR_CORE_NAVIGATION_COMMANDS = [ SPLIT_EDITOR, CLOSE_EDITOR_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, - UNLOCK_GROUP_COMMAND_ID + UNLOCK_GROUP_COMMAND_ID, + UNMAXIMIZE_EDITOR_GROUP ]; export interface ActiveEditorMoveCopyArguments { @@ -1464,7 +1468,7 @@ function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | }; } -function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { +export function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { if (URI.isUri(resourceOrContext)) { return context; } @@ -1480,7 +1484,7 @@ function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, co return undefined; } -function resolveCommandsContext(editorGroupService: IEditorGroupsService, context?: IEditorCommandsContext): { group: IEditorGroup; editor?: EditorInput } { +export function resolveCommandsContext(editorGroupService: IEditorGroupsService, context?: IEditorCommandsContext): { group: IEditorGroup; editor?: EditorInput } { // Resolve from context let group = context && typeof context.groupId === 'number' ? editorGroupService.getGroup(context.groupId) : undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index c21abb83198..28e554f316c 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -101,6 +101,9 @@ export class EditorPart extends Part implements IEditorPart { private readonly _onDidChangeGroupLocked = this._register(new Emitter()); readonly onDidChangeGroupLocked = this._onDidChangeGroupLocked.event; + private readonly _onDidChangeGroupMaximized = this._register(new Emitter()); + readonly onDidChangeGroupMaximized = this._onDidChangeGroupMaximized.event; + private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; @@ -137,6 +140,7 @@ export class EditorPart extends Part implements IEditorPart { private centeredLayoutWidget!: CenteredViewLayout; private gridWidget!: SerializableGrid; + private readonly gridWidgetDisposables = this._register(new DisposableStore()); private readonly gridWidgetView = this._register(new GridWidgetView()); constructor( @@ -147,7 +151,7 @@ export class EditorPart extends Part implements IEditorPart { @IThemeService themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService storageService: IStorageService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, ) { super(id, { hasTitle: false }, themeService, storageService, layoutService); @@ -369,21 +373,48 @@ export class EditorPart extends Part implements IEditorPart { this.gridWidget.distributeViewSizes(); break; case GroupsArrangement.MAXIMIZE: - this.gridWidget.maximizeViewSize(target); - break; - case GroupsArrangement.TOGGLE: - if (this.isGroupMaximized(target)) { - this.arrangeGroups(GroupsArrangement.EVEN); - } else { - this.arrangeGroups(GroupsArrangement.MAXIMIZE); + if (this.groups.length < 2) { + return; // need at least 2 groups to be maximized } - + this.gridWidget.maximizeView(target); + this.doSetGroupActive(target); + break; + case GroupsArrangement.EXPAND: + this.gridWidget.expandView(target); break; } } - isGroupMaximized(targetGroup: IEditorGroupView): boolean { - return this.gridWidget.isViewSizeMaximized(targetGroup); + toggleMaximizeGroup(target: IEditorGroupView = this.activeGroup): void { + if (this.hasMaximizedGroup()) { + this.unmaximizeGroup(); + } else { + this.arrangeGroups(GroupsArrangement.MAXIMIZE, target); + } + } + + toggleExpandGroup(target: IEditorGroupView = this.activeGroup): void { + if (this.isGroupExpanded(this.activeGroup)) { + this.arrangeGroups(GroupsArrangement.EVEN); + } else { + this.arrangeGroups(GroupsArrangement.EXPAND, target); + } + } + + private unmaximizeGroup(): void { + this.gridWidget.exitMaximizedView(); + } + + private hasMaximizedGroup(): boolean { + return this.gridWidget.hasMaximizedView(); + } + + private isGroupMaximized(targetGroup: IEditorGroupView): boolean { + return this.gridWidget.isViewMaximized(targetGroup); + } + + isGroupExpanded(targetGroup: IEditorGroupView): boolean { + return this.gridWidget.isViewExpanded(targetGroup); } setGroupOrientation(orientation: GroupOrientation): void { @@ -524,7 +555,7 @@ export class EditorPart extends Part implements IEditorPart { if (locationView.groupsView === this) { const restoreFocus = this.shouldRestoreFocus(locationView.element); - const shouldMaximize = this.groupViews.size > 1 && this.isGroupMaximized(locationView); + const shouldExpand = this.groupViews.size > 1 && this.isGroupExpanded(locationView); newGroupView = this.doCreateGroupView(groupToCopy); // Add to grid widget @@ -544,9 +575,9 @@ export class EditorPart extends Part implements IEditorPart { // Notify group index change given a new group was added this.notifyGroupIndexChange(); - // Maximize new group, if the reference view was previously maximized - if (shouldMaximize) { - this.arrangeGroups(GroupsArrangement.MAXIMIZE, newGroupView); + // Expand new group, if the reference view was previously expanded + if (shouldExpand) { + this.arrangeGroups(GroupsArrangement.EXPAND, newGroupView); } // Restore focus if we had it previously after completing the grid @@ -642,7 +673,7 @@ export class EditorPart extends Part implements IEditorPart { // Mark group as new active group.setActive(true); - // Maximize the group if it is currently minimized + // Expand the group if it is currently minimized this.doRestoreGroup(group); // Event @@ -657,9 +688,13 @@ export class EditorPart extends Part implements IEditorPart { private doRestoreGroup(group: IEditorGroupView): void { if (this.gridWidget) { + if (this.hasMaximizedGroup() && !this.isGroupMaximized(group)) { + this.unmaximizeGroup(); + } + const viewSize = this.gridWidget.getViewSize(group); if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) { - this.arrangeGroups(GroupsArrangement.MAXIMIZE, group); + this.arrangeGroups(GroupsArrangement.EXPAND, group); } } } @@ -1032,6 +1067,10 @@ export class EditorPart extends Part implements IEditorPart { } centerLayout(active: boolean): void { + if (this.hasMaximizedGroup()) { + this.unmaximizeGroup(); + } + this.centeredLayoutWidget.activate(active); this._activeGroup.focus(); @@ -1158,6 +1197,10 @@ export class EditorPart extends Part implements IEditorPart { this._onDidChangeSizeConstraints.input = gridWidget.onDidChange; this._onDidScroll.input = gridWidget.onDidScroll; + this.gridWidgetDisposables.clear(); + this.gridWidgetDisposables.add(gridWidget.onDidChangeViewMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); + + this._onDidChangeGroupMaximized.fire(this.hasMaximizedGroup()); this.onDidSetGridWidget.fire(undefined); } diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index eb45a93eb54..92c96fced43 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -97,6 +97,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); + disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); disposables.add(part.onDidChangeGroupIndex(group => this._onDidChangeGroupIndex.fire(group))); disposables.add(part.onDidChangeGroupLocked(group => this._onDidChangeGroupLocked.fire(group))); @@ -176,6 +177,9 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd private readonly _onDidChangeGroupLocked = this._register(new Emitter()); readonly onDidChangeGroupLocked = this._onDidChangeGroupLocked.event; + private readonly _onDidChangeGroupMaximized = this._register(new Emitter()); + readonly onDidChangeGroupMaximized = this._onDidChangeGroupMaximized.event; + //#endregion //#region Editor Groups Service @@ -227,11 +231,19 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd } setSize(group: IEditorGroupView | GroupIdentifier, size: { width: number; height: number }): void { - return this.getPart(group).setSize(group, size); + this.getPart(group).setSize(group, size); } - arrangeGroups(arrangement: GroupsArrangement): void { - return this.activePart.arrangeGroups(arrangement); + arrangeGroups(arrangement: GroupsArrangement, group?: IEditorGroupView): void { + (group !== undefined ? this.getPart(group) : this.activePart).arrangeGroups(arrangement, group); + } + + toggleMaximizeGroup(group?: IEditorGroupView): void { + (group !== undefined ? this.getPart(group) : this.activePart).toggleMaximizeGroup(group); + } + + toggleExpandGroup(group?: IEditorGroupView): void { + (group !== undefined ? this.getPart(group) : this.activePart).toggleExpandGroup(group); } restoreGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { @@ -239,7 +251,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd } applyLayout(layout: EditorGroupLayout): void { - return this.activePart.applyLayout(layout); + this.activePart.applyLayout(layout); } getLayout(): EditorGroupLayout { @@ -247,7 +259,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd } centerLayout(active: boolean): void { - return this.activePart.centerLayout(active); + this.activePart.centerLayout(active); } isLayoutCentered(): boolean { @@ -259,7 +271,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd } setGroupOrientation(orientation: GroupOrientation): void { - return this.activePart.setGroupOrientation(orientation); + this.activePart.setGroupOrientation(orientation); } findGroup(scope: IFindGroupScope, source?: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView | undefined { @@ -275,7 +287,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd } removeGroup(group: IEditorGroupView | GroupIdentifier): void { - return this.getPart(group).removeGroup(group); + this.getPart(group).removeGroup(group); } moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView { diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index af5579120fd..a9ed03f97c2 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -31,7 +31,7 @@ import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platf import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { MergeGroupMode, IMergeGroupOptions, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; @@ -992,9 +992,17 @@ export class MultiEditorTabsControl extends EditorTabsControl { const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor && this.tabsModel.isPinned(editor)) { - if (this.groupsView.partOptions.doubleClickTabToToggleEditorGroupSizes) { - this.groupsView.arrangeGroups(GroupsArrangement.TOGGLE, this.groupView); + switch (this.groupsView.partOptions.doubleClickTabToToggleEditorGroupSizes) { + case 'maximize': + this.groupsView.toggleMaximizeGroup(this.groupView); + break; + case 'expand': + this.groupsView.toggleExpandGroup(this.groupView); + break; + case 'off': + break; } + } else { this.groupView.pinEditor(editor); } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index c6c287adb4d..217d2623382 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -322,9 +322,15 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('centeredLayoutDynamicWidth', "Controls whether the centered layout tries to maintain constant width when the window is resized.") }, 'workbench.editor.doubleClickTabToToggleEditorGroupSizes': { - 'type': 'boolean', - 'default': true, - 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'doubleClickTabToToggleEditorGroupSizes' }, "Controls whether to maximize/restore the editor group when double clicking on a tab. This value is ignored when `#workbench.editor.showTabs#` is not set to `multiple`.") + 'type': 'string', + 'enum': ['maximize', 'expand', 'off'], + 'default': 'expand', + 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'doubleClickTabToToggleEditorGroupSizes' }, "Controls how the editor group is resized when double clicking on a tab. This value is ignored when `#workbench.editor.showTabs#` is not set to `multiple`."), + 'enumDescriptions': [ + localize('workbench.editor.doubleClickTabToToggleEditorGroupSizes.maximize', "All other editor groups are hidden and the current editor group is maximized to take up the entire editor area."), + localize('workbench.editor.doubleClickTabToToggleEditorGroupSizes.expand', "The editor group takes as much space as possible by making all other editor groups as small as possible."), + localize('workbench.editor.doubleClickTabToToggleEditorGroupSizes.off', "No editor group is resized when double clicking on a tab.") + ] }, 'workbench.editor.limit.enabled': { 'type': 'boolean', @@ -780,16 +786,20 @@ Registry.as(Extensions.ConfigurationMigration) Registry.as(Extensions.ConfigurationMigration) .registerConfigurationMigrations([{ + key: 'workbench.editor.doubleClickTabToToggleEditorGroupSizes', migrateFn: (value: any) => { + if (typeof value === 'boolean') { + value = value ? 'expand' : 'off'; + } + return [['workbench.editor.doubleClickTabToToggleEditorGroupSizes', { value: value }]]; + } + }, { key: 'workbench.editor.showTabs', migrateFn: (value: any) => { if (typeof value === 'boolean') { value = value ? 'multiple' : 'single'; } return [['workbench.editor.showTabs', { value: value }]]; } - }]); - -Registry.as(Extensions.ConfigurationMigration) - .registerConfigurationMigrations([{ + }, { key: 'zenMode.hideTabs', migrateFn: (value: any) => { const result: ConfigurationKeyValuePairs = [['zenMode.hideTabs', { value: undefined }]]; if (value === true) { diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index c58fb9d32f7..d58629b85f7 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -79,6 +79,7 @@ export const SplitEditorsVertically = new RawContextKey('splitEditorsVe export const EditorAreaVisibleContext = new RawContextKey('editorAreaVisible', true, localize('editorAreaVisible', "Whether the editor area is visible")); export const EditorTabsVisibleContext = new RawContextKey('editorTabsVisible', true, localize('editorTabsVisible', "Whether editor tabs are visible")); export const EditorPinnedAndUnpinnedTabsContext = new RawContextKey('editorPinnedAndUnpinnedTabsVisible', false, true); +export const MaximizedEditorGroupContext = new RawContextKey('maximizedEditorGroup', false, localize('editorGroupMaximized', "Editor group is maximized")); //#endregion diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 60e2e472be2..e7854dea7b5 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1139,7 +1139,7 @@ interface IEditorPartConfiguration { splitSizing?: 'auto' | 'split' | 'distribute'; splitOnDragAndDrop?: boolean; centeredLayoutFixedWidth?: boolean; - doubleClickTabToToggleEditorGroupSizes?: boolean; + doubleClickTabToToggleEditorGroupSizes?: 'maximize' | 'expand' | 'off'; limit?: IEditorPartLimitConfiguration; decorations?: IEditorPartDecorationsConfiguration; } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 1b48ec30bca..ba0188a48b8 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -43,23 +43,22 @@ export interface IFindGroupScope { } export const enum GroupsArrangement { + /** + * Make the current active group consume the entire + * editor area. + */ + MAXIMIZE, /** * Make the current active group consume the maximum * amount of space possible. */ - MAXIMIZE, + EXPAND, /** * Size all groups evenly. */ - EVEN, - - /** - * Will behave like MINIMIZE_OTHERS if the active - * group is not already maximized and EVEN otherwise - */ - TOGGLE + EVEN } export interface GroupLayoutArgument { @@ -215,6 +214,11 @@ export interface IEditorGroupsContainer { */ readonly onDidChangeGroupLocked: Event; + /** + * An event for when the maximized state of a group changes. + */ + readonly onDidChangeGroupMaximized: Event; + /** * An active group is the default location for new editors to open. */ @@ -273,7 +277,17 @@ export interface IEditorGroupsContainer { /** * Arrange all groups in the container according to the provided arrangement. */ - arrangeGroups(arrangement: GroupsArrangement): void; + arrangeGroups(arrangement: GroupsArrangement, target?: IEditorGroup | GroupIdentifier): void; + + /** + * Toggles the target goup size to maximize/unmaximize. + */ + toggleMaximizeGroup(group?: IEditorGroup | GroupIdentifier): void; + + /** + * Toggles the target goup size to expand/distribute even. + */ + toggleExpandGroup(group?: IEditorGroup | GroupIdentifier): void; /** * Applies the provided layout by either moving existing groups or creating new groups. diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 02d63db45ad..0e8a0e2b313 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart, ITestInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -1718,5 +1718,44 @@ suite('EditorGroupsService', () => { assert.strictEqual(rootGroup.isLocked, false); }); + test('maximize editor group', async () => { + const instantiationService = workbenchInstantiationService(undefined, disposables); + const [part] = await createPart(instantiationService); + + const rootGroup = part.activeGroup; + const editorPartSize = part.getSize(rootGroup); + + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const rightBottomGroup = part.addGroup(rightGroup, GroupDirection.DOWN); + + const sizeRootGroup = part.getSize(rootGroup); + const sizeRightGroup = part.getSize(rightGroup); + const sizeRightBottomGroup = part.getSize(rightBottomGroup); + + let maximizedValue; + const maxiizeGroupEventDisposable = part.onDidChangeGroupMaximized((maximized) => { + maximizedValue = maximized; + }); + + part.arrangeGroups(GroupsArrangement.MAXIMIZE, rootGroup); + + // getSize() + assert.deepStrictEqual(part.getSize(rootGroup), editorPartSize); + assert.deepStrictEqual(part.getSize(rightGroup), { width: 0, height: 0 }); + assert.deepStrictEqual(part.getSize(rightBottomGroup), { width: 0, height: 0 }); + + assert.deepStrictEqual(maximizedValue, true); + + part.toggleMaximizeGroup(); + + // Size is restored + assert.deepStrictEqual(part.getSize(rootGroup), sizeRootGroup); + assert.deepStrictEqual(part.getSize(rightGroup), sizeRightGroup); + assert.deepStrictEqual(part.getSize(rightBottomGroup), sizeRightBottomGroup); + + assert.deepStrictEqual(maximizedValue, false); + maxiizeGroupEventDisposable.dispose(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index b645db393f7..f8571e68675 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -1691,7 +1691,7 @@ suite('EditorService', () => { editor = await service.openEditor(input2, { pinned: true, activation: EditorActivation.ACTIVATE }, sideGroup); assert.strictEqual(part.activeGroup, sideGroup); - part.arrangeGroups(GroupsArrangement.MAXIMIZE); + part.arrangeGroups(GroupsArrangement.EXPAND); editor = await service.openEditor(input1, { pinned: true, preserveFocus: true, activation: EditorActivation.RESTORE }, rootGroup); assert.strictEqual(part.activeGroup, sideGroup); }); @@ -1711,13 +1711,13 @@ suite('EditorService', () => { assert.strictEqual(part.activeGroup, sideGroup); assert.notStrictEqual(rootGroup, sideGroup); - part.arrangeGroups(GroupsArrangement.MAXIMIZE, part.activeGroup); + part.arrangeGroups(GroupsArrangement.EXPAND, part.activeGroup); await rootGroup.closeEditor(input2); assert.strictEqual(part.activeGroup, sideGroup); - assert(!part.isGroupMaximized(rootGroup)); - assert(part.isGroupMaximized(part.activeGroup)); + assert(!part.isGroupExpanded(rootGroup)); + assert(part.isGroupExpanded(part.activeGroup)); }); test('active editor change / visible editor change events', async function () { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index fa0abcf5d20..8f1a7006ef3 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -824,6 +824,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { onDidMoveGroup: Event = Event.None; onDidChangeGroupIndex: Event = Event.None; onDidChangeGroupLocked: Event = Event.None; + onDidChangeGroupMaximized: Event = Event.None; onDidLayout: Event = Event.None; onDidChangeEditorPartOptions = Event.None; onDidScroll = Event.None; @@ -850,6 +851,8 @@ export class TestEditorGroupsService implements IEditorGroupsService { getSize(_group: number | IEditorGroup): { width: number; height: number } { return { width: 100, height: 100 }; } setSize(_group: number | IEditorGroup, _size: { width: number; height: number }): void { } arrangeGroups(_arrangement: GroupsArrangement): void { } + toggleMaximizeGroup(): void { } + toggleExpandGroup(): void { } applyLayout(_layout: EditorGroupLayout): void { } getLayout(): EditorGroupLayout { throw new Error('not implemented'); } setGroupOrientation(_orientation: GroupOrientation): void { } @@ -966,6 +969,8 @@ export class TestEditorGroupAccessor implements IEditorGroupsView { copyGroup(group: number | IEditorGroupView, location: number | IEditorGroupView, direction: GroupDirection): IEditorGroupView { throw new Error('Method not implemented.'); } removeGroup(group: number | IEditorGroupView): void { throw new Error('Method not implemented.'); } arrangeGroups(arrangement: GroupsArrangement, target?: number | IEditorGroupView | undefined): void { throw new Error('Method not implemented.'); } + toggleMaximizeGroup(group: number | IEditorGroupView): void { throw new Error('Method not implemented.'); } + toggleExpandGroup(group: number | IEditorGroupView): void { throw new Error('Method not implemented.'); } } export class TestEditorService implements EditorServiceImpl { From 92d0c382a51375dcdf9c26dddd01f8c9a1eceb5f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:56:15 +0200 Subject: [PATCH 14/21] API - remove contribSourceControlInputBoxMenu proposal (#196244) --- src/vs/platform/actions/common/actions.ts | 1 - src/vs/workbench/contrib/scm/browser/menus.ts | 10 ---------- src/vs/workbench/contrib/scm/common/scm.ts | 1 - .../services/actions/common/menusExtensionPoint.ts | 6 ------ .../extensions/common/extensionsApiProposals.ts | 1 - ...code.proposed.contribSourceControlInputBoxMenu.d.ts | 7 ------- 6 files changed, 26 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 2dafc74b618..081100f8528 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -110,7 +110,6 @@ export class MenuId { static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext'); static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext'); static readonly SCMSourceControl = new MenuId('SCMSourceControl'); - static readonly SCMInputBox = new MenuId('SCMInputBox'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly SearchActionMenu = new MenuId('SearchActionContext'); diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index d1bde83d780..9918414d4c0 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -161,16 +161,6 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { return this._repositoryMenu; } - private _inputBoxMenu: IMenu | undefined; - get inputBoxMenu(): IMenu { - if (!this._inputBoxMenu) { - this._inputBoxMenu = this.menuService.createMenu(MenuId.SCMInputBox, this.contextKeyService); - this.disposables.add(this._inputBoxMenu); - } - - return this._inputBoxMenu; - } - private readonly disposables = new DisposableStore(); constructor( diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 4327c3c83d8..3e52814ee60 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -181,7 +181,6 @@ export interface ISCMTitleMenu { export interface ISCMRepositoryMenus { readonly titleMenu: ISCMTitleMenu; readonly repositoryMenu: IMenu; - readonly inputBoxMenu: IMenu; getResourceGroupMenu(group: ISCMResourceGroup): IMenu; getResourceMenu(resource: ISCMResource): IMenu; getResourceFolderMenu(group: ISCMResourceGroup): IMenu; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 56609d8331f..bb1993e9d95 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -124,12 +124,6 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.SCMSourceControl, description: localize('menus.scmSourceControl', "The Source Control menu") }, - { - key: 'scm/inputBox', - id: MenuId.SCMInputBox, - description: localize('menus.scmInputBox', "The Source Control input box menu"), - proposed: 'contribSourceControlInputBoxMenu' - }, { key: 'scm/resourceState/context', id: MenuId.SCMResourceContext, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index b76468a8e26..cb95f2c976f 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -31,7 +31,6 @@ export const allApiProposals = Object.freeze({ contribNotebookStaticPreloads: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', - contribSourceControlInputBoxMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts b/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts deleted file mode 100644 index 4774bf46fef..00000000000 --- a/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// empty placeholder declaration for the `scm/inputBox` menu contribution point -// https://github.com/microsoft/vscode/issues/195474 From 149ec58a5a0693d4a019001cc13e6c4cd8ceb34b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:56:28 +0200 Subject: [PATCH 15/21] SCM - overwrite cancel icon color (#196243) --- src/vs/workbench/contrib/scm/browser/media/scm.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 16274449aff..3b6417cc287 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -260,6 +260,10 @@ outline-offset: -1px; } +.scm-view .scm-input .actions .action-label.codicon.codicon-debug-stop { + color: var(--vscode-icon-foreground) !important; +} + .scm-view .scm-editor-container .monaco-editor { border-radius: 2px; } From 8c0b491626d5364ab22894a93e24f2ae44bc24fb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:56:46 +0200 Subject: [PATCH 16/21] SCM - fix rendering issue with input box placeholder (#196241) --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index ce8d71d993e..492d2e20979 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1960,6 +1960,9 @@ class SCMInputWidget { // ActionBar const onDidChangeActionButton = () => { + // Update placeholder width to accommodate for the action bar + this.placeholderTextContainer.style.width = input.actionButton ? 'calc(100% - 26px)' : '100%'; + this.actionBar.clear(); if (!input.actionButton) { return; @@ -1973,9 +1976,6 @@ class SCMInputWidget { () => this.commandService.executeCommand(input.actionButton!.command.id, ...(input.actionButton!.command.arguments || []))); this.actionBar.push(action, { icon: true, label: false }); - - // Update placeholder width to accommodate for the action bar - this.placeholderTextContainer.style.width = input.actionButton ? 'calc(100% - 26px)' : '100%'; }; this.repositoryDisposables.add(input.onDidChangeActionButton(onDidChangeActionButton, this)); From c7f1a4e36d79c5ef63c9265cbd524e7437140a1f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Oct 2023 13:01:50 +0200 Subject: [PATCH 17/21] editor - fix progress bar position (#196245) --- src/vs/workbench/browser/media/part.css | 8 ++------ src/vs/workbench/browser/parts/editor/editorGroupView.ts | 1 + .../browser/parts/editor/media/editorgroupview.css | 5 ++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index a0628d68945..526b2b541de 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -87,7 +87,7 @@ } .monaco-workbench .part > .content > .monaco-progress-container, -.monaco-workbench .part.editor > .content .monaco-progress-container { +.monaco-workbench .part.editor > .content .editor-group-container > .monaco-progress-container { position: absolute; left: 0; top: 33px; /* at the bottom of the 35px height title container */ @@ -96,10 +96,6 @@ } .monaco-workbench .part > .content > .monaco-progress-container .progress-bit, -.monaco-workbench - .part.editor - > .content - .monaco-progress-container - .progress-bit { +.monaco-workbench .part.editor > .content .editor-group-container > .monaco-progress-container .progress-bit { height: 2px; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index b898596878f..6883d55ac43 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -700,6 +700,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control switch between singleEditorTabs, multiEditorTabs and multiRowEditorTabs if ( event.oldPartOptions.showTabs !== event.newPartOptions.showTabs || + event.oldPartOptions.tabHeight !== event.newPartOptions.tabHeight || (event.oldPartOptions.showTabs === 'multiple' && event.oldPartOptions.pinnedTabsOnSeparateRow !== event.newPartOptions.pinnedTabsOnSeparateRow) ) { diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index f61bb8aaa45..80d7062ec80 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -186,7 +186,6 @@ height: 100%; } -/* Override top position of progress bar which defined in vs/workbench/browser/media/part.css */ -.monaco-workbench .part.editor > .content .monaco-progress-container { - top: min(calc(var(--editor-group-tabs-height) - 2px), 0); +.monaco-workbench .part.editor > .content .editor-group-container > .monaco-progress-container { + top: max(calc(var(--editor-group-tabs-height) - 2px), 0px); /* Override top position of progress bar which defined in vs/workbench/browser/media/part.css */ } From e55dc5d265594b6aaae35a4d0820b9af796dd851 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 23 Oct 2023 13:02:15 +0200 Subject: [PATCH 18/21] fix #196149 (#196246) --- .../browser/parts/panel/media/panelpart.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 0cb8bea4fa7..5b3bf23fdc9 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -53,3 +53,19 @@ .monaco-workbench .part.panel > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item:focus { outline: none; } + +/* Rotate icons when panel is on right */ +.monaco-workbench .part.basepanel.right .title-actions .codicon-split-horizontal::before, +.monaco-workbench .part.basepanel.right .global-actions .codicon-panel-maximize::before, +.monaco-workbench .part.basepanel.right .global-actions .codicon-panel-restore::before { + display: inline-block; + transform: rotate(-90deg); +} + +/* Rotate icons when panel is on left */ +.monaco-workbench .part.basepanel.left .title-actions .codicon-split-horizontal::before, +.monaco-workbench .part.basepanel.left .global-actions .codicon-panel-maximize::before, +.monaco-workbench .part.basepanel.left .global-actions .codicon-panel-restore::before { + display: inline-block; + transform: rotate(90deg); +} From 27265b038df8658ea64b58caf14ffcfddab9bd98 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 23 Oct 2023 12:44:46 +0200 Subject: [PATCH 19/21] Fixes #189962 --- src/vs/base/common/observableInternal/base.ts | 9 ++ .../inlineCompletions/browser/commands.ts | 7 +- .../browser/inlineCompletionsController.ts | 85 +++++++++++-------- .../browser/inlineCompletionsModel.ts | 8 +- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 321ec278ff2..bc7f900caed 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -231,6 +231,15 @@ export function transaction(fn: (tx: ITransaction) => void, getDebugName?: () => } } +export async function asyncTransaction(fn: (tx: ITransaction) => Promise, getDebugName?: () => string): Promise { + const tx = new TransactionImpl(fn, getDebugName); + try { + await fn(tx); + } finally { + tx.finish(); + } +} + export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void { if (!tx) { transaction(fn, getDebugName); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/commands.ts index ac7535f9231..9727bec2359 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/commands.ts @@ -5,6 +5,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { transaction } from 'vs/base/common/observable'; +import { asyncTransaction } from 'vs/base/common/observableInternal/base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -72,7 +73,11 @@ export class TriggerInlineSuggestionAction extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineCompletionsController.get(editor); - controller?.model.get()?.triggerExplicitly(); + await asyncTransaction(async tx => { + /** @description triggerExplicitly from command */ + await controller?.model.get()?.triggerExplicitly(tx); + controller?.playAudioCue(tx); + }); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index f9697ac3232..4f977c5df2e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -6,7 +6,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ITransaction, autorun, constObservable, disposableObservableValue, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { ITransaction, autorun, autorunHandleChanges, constObservable, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction } from 'vs/base/common/observable'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -38,9 +38,9 @@ export class InlineCompletionsController extends Disposable { } public readonly model = disposableObservableValue('inlineCompletionModel', undefined); - private readonly textModelVersionId = observableValue(this, -1); - private readonly cursorPosition = observableValue(this, new Position(1, 1)); - private readonly suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( + private readonly _textModelVersionId = observableValue(this, -1); + private readonly _cursorPosition = observableValue(this, new Position(1, 1)); + private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( this.editor, () => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined), (tx) => this.updateObservables(tx, VersionIdChangeReason.Other), @@ -54,32 +54,34 @@ export class InlineCompletionsController extends Disposable { )); private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); - private ghostTextWidget = this._register(this.instantiationService.createInstance(GhostTextWidget, this.editor, { + private _ghostTextWidget = this._register(this._instantiationService.createInstance(GhostTextWidget, this.editor, { ghostText: this.model.map((v, reader) => /** ghostText */ v?.ghostText.read(reader)), minReservedLineCount: constObservable(0), targetTextModel: this.model.map(v => v?.textModel), })); - private readonly _debounceValue = this.debounceService.for( - this.languageFeaturesService.inlineCompletionsProvider, + private readonly _debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, 'InlineCompletionsDebounce', { min: 50, max: 50 } ); + private readonly _playAudioCueSignal = observableSignal(this); + constructor( public readonly editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICommandService private readonly commandService: ICommandService, - @ILanguageFeatureDebounceService private readonly debounceService: ILanguageFeatureDebounceService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IAudioCueService private readonly audioCueService: IAudioCueService, - @IKeybindingService private readonly _keybindingService: IKeybindingService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ICommandService private readonly _commandService: ICommandService, + @ILanguageFeatureDebounceService private readonly _debounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IAudioCueService private readonly _audioCueService: IAudioCueService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); - this._register(new InlineCompletionContextKeys(this.contextKeyService, this.model)); + this._register(new InlineCompletionContextKeys(this._contextKeyService, this.model)); this._register(Event.runAndSubscribe(editor.onDidChangeModel, () => transaction(tx => { /** @description onDidChangeModel */ this.model.set(undefined, tx); @@ -87,12 +89,12 @@ export class InlineCompletionsController extends Disposable { const textModel = editor.getModel(); if (textModel) { - const model = instantiationService.createInstance( + const model = _instantiationService.createInstance( InlineCompletionsModel, textModel, - this.suggestWidgetAdaptor.selectedItem, - this.cursorPosition, - this.textModelVersionId, + this._suggestWidgetAdaptor.selectedItem, + this._cursorPosition, + this._textModelVersionId, this._debounceValue, observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview), observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).previewMode), @@ -130,7 +132,7 @@ export class InlineCompletionsController extends Disposable { } }))); - this._register(this.commandService.onDidExecuteCommand((e) => { + this._register(this._commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. const commands = new Set([ CoreEditingCommands.Tab.id, @@ -149,7 +151,7 @@ export class InlineCompletionsController extends Disposable { this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging - if (this.contextKeyService.getContextKeyValue('accessibleViewIsShown') || this.configurationService.getValue('editor.inlineSuggest.keepOnBlur') || + if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') || editor.getOption(EditorOption.inlineSuggest).keepOnBlur) { return; } @@ -167,19 +169,28 @@ export class InlineCompletionsController extends Disposable { const state = this.model.read(reader)?.state.read(reader); if (state?.suggestItem) { if (state.ghostText.lineCount >= 2) { - this.suggestWidgetAdaptor.forceRenderingAbove(); + this._suggestWidgetAdaptor.forceRenderingAbove(); } } else { - this.suggestWidgetAdaptor.stopForceRenderingAbove(); + this._suggestWidgetAdaptor.stopForceRenderingAbove(); } })); this._register(toDisposable(() => { - this.suggestWidgetAdaptor.stopForceRenderingAbove(); + this._suggestWidgetAdaptor.stopForceRenderingAbove(); })); let lastInlineCompletionId: string | undefined = undefined; - this._register(autorun(reader => { + this._register(autorunHandleChanges({ + handleChange: (context, changeSummary) => { + if (context.didChange(this._playAudioCueSignal)) { + lastInlineCompletionId = undefined; + } + return true; + }, + }, async reader => { /** @description play audio cue & read suggestion */ + this._playAudioCueSignal.read(reader); + const model = this.model.read(reader); const state = model?.state.read(reader); if (!model || !state || !state.inlineCompletion) { @@ -190,7 +201,7 @@ export class InlineCompletionsController extends Disposable { if (state.inlineCompletion.semanticId !== lastInlineCompletionId) { lastInlineCompletionId = state.inlineCompletion.semanticId; const lineText = model.textModel.getLineContent(state.ghostText.lineNumber); - this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => { + this._audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => { if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { this.provideScreenReaderUpdate(state.ghostText.renderForScreenReader(lineText)); } @@ -198,17 +209,21 @@ export class InlineCompletionsController extends Disposable { } })); - this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this.instantiationService)); - this._register(this.configurationService.onDidChangeConfiguration(e => { + this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this._instantiationService)); + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) { - this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this.configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); } })); - this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this.configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + } + + public playAudioCue(tx: ITransaction) { + this._playAudioCueSignal.trigger(tx); } private provideScreenReaderUpdate(content: string): void { - const accessibleViewShowing = this.contextKeyService.getContextKeyValue('accessibleViewIsShown'); + const accessibleViewShowing = this._contextKeyService.getContextKeyValue('accessibleViewIsShown'); const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView'); let hint: string | undefined; if (!accessibleViewShowing && accessibleViewKeybinding && this.editor.getOption(EditorOption.inlineCompletionsAccessibilityVerbose)) { @@ -224,8 +239,8 @@ export class InlineCompletionsController extends Disposable { */ private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void { const newModel = this.editor.getModel(); - this.textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason); - this.cursorPosition.set(this.editor.getPosition() ?? new Position(1, 1), tx); + this._textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason); + this._cursorPosition.set(this.editor.getPosition() ?? new Position(1, 1), tx); } public shouldShowHoverAt(range: Range) { @@ -237,7 +252,7 @@ export class InlineCompletionsController extends Disposable { } public shouldShowHoverAtViewZone(viewZoneId: string): boolean { - return this.ghostTextWidget.ownsViewZone(viewZoneId); + return this._ghostTextWidget.ownsViewZone(viewZoneId); } public hide() { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 2aa8e7cd21f..1a7663d40ee 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -35,7 +35,7 @@ export enum VersionIdChangeReason { export class InlineCompletionsModel extends Disposable { private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this.textModelVersionId, this._debounceValue)); private readonly _isActive = observableValue(this, false); - private readonly _forceUpdate = observableSignal('forceUpdate'); + readonly _forceUpdateSignal = observableSignal('forceUpdate'); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); @@ -92,13 +92,13 @@ export class InlineCompletionsModel extends Disposable { /** @description fetch inline completions */ if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(ctx.change)) { changeSummary.preserveCurrentCompletion = true; - } else if (ctx.didChange(this._forceUpdate)) { + } else if (ctx.didChange(this._forceUpdateSignal)) { changeSummary.inlineCompletionTriggerKind = ctx.change; } return true; }, }, (reader, changeSummary) => { - this._forceUpdate.read(reader); + this._forceUpdateSignal.read(reader); const shouldUpdate = (this._enabled.read(reader) && this.selectedSuggestItem.read(reader)) || this._isActive.read(reader); if (!shouldUpdate) { this._source.cancelUpdate(); @@ -140,7 +140,7 @@ export class InlineCompletionsModel extends Disposable { public async triggerExplicitly(tx?: ITransaction): Promise { subtransaction(tx, tx => { this._isActive.set(true, tx); - this._forceUpdate.trigger(tx, InlineCompletionTriggerKind.Explicit); + this._forceUpdateSignal.trigger(tx, InlineCompletionTriggerKind.Explicit); }); await this._fetchInlineCompletions.get(); } From d4586b43cdc35128498bf62e78ed7b70ba119290 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Tue, 3 Oct 2023 15:36:12 +0200 Subject: [PATCH 20/21] git: GitError captures stacktrace --- extensions/git/src/git.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 2a3fc098d52..0499d04dff2 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -267,10 +267,9 @@ export interface IGitErrorData { gitArgs?: string[]; } -export class GitError { +export class GitError extends Error { error?: Error; - message: string; stdout?: string; stderr?: string; exitCode?: number; @@ -279,15 +278,9 @@ export class GitError { gitArgs?: string[]; constructor(data: IGitErrorData) { - if (data.error) { - this.error = data.error; - this.message = data.error.message; - } else { - this.error = undefined; - this.message = ''; - } + super(data.error?.message || data.message || 'Git error'); - this.message = this.message || data.message || 'Git error'; + this.error = data.error; this.stdout = data.stdout; this.stderr = data.stderr; this.exitCode = data.exitCode; @@ -296,7 +289,7 @@ export class GitError { this.gitArgs = data.gitArgs; } - toString(): string { + override toString(): string { let result = this.message + ' ' + JSON.stringify({ exitCode: this.exitCode, gitErrorCode: this.gitErrorCode, From c6dcdc753ba263e321e907eb92270ea18ef19ee8 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Tue, 3 Oct 2023 15:36:43 +0200 Subject: [PATCH 21/21] git: print more info when relative path not found --- extensions/git/src/git.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 0499d04dff2..17e17c8c7f9 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1166,7 +1166,9 @@ export class Repository { const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; if (!element) { - throw new GitError({ message: 'Git relative path not found.' }); + throw new GitError({ + message: `Git relative path not found. Was looking for ${relativePathLowercase} among ${JSON.stringify(elements.map(({ file }) => file), null, 2)}`, + }); } return element.file;