From bac1926029c174998664be4a0ae977e2e787481d Mon Sep 17 00:00:00 2001 From: Russell Davis <551404+russelldavis@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:30:41 -0700 Subject: [PATCH 001/141] Fix decreaseIndentPattern for javascript and typescript Fixes #201424 It wasn't matching closing parens, which resulted in these issues: * Pressing enter with just a closing paren to the right of the caret wouldn't result in a dedent on the next line * With the caret at the start of the line below a line containing only a closing paren, pressing tab would result in an extra level of indentation --- extensions/javascript/javascript-language-configuration.json | 2 +- extensions/typescript-basics/language-configuration.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 4029985233a..12f6e5cac1f 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -111,7 +111,7 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$" + "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index 03f06fa04d5..45657928dbc 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -129,7 +129,7 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$" + "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" From 4d542ef5d144449f1c399dc5c06cbf296380b7fe Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Tue, 13 Feb 2024 13:55:49 +0000 Subject: [PATCH 002/141] Make channel log level settable from output view --- .../contrib/logs/common/logs.contribution.ts | 4 +- .../contrib/logs/common/logsActions.ts | 17 ++++++--- .../output/browser/output.contribution.ts | 37 ++++++++++++++++++- .../contrib/output/browser/outputServices.ts | 9 ++++- .../services/output/common/output.ts | 2 + 5 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 19257411641..2b95a4341ce 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -36,8 +36,8 @@ registerAction2(class extends Action2 { f1: true }); } - run(servicesAccessor: ServicesAccessor): Promise { - return servicesAccessor.get(IInstantiationService).createInstance(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.TITLE.value).run(); + run(servicesAccessor: ServicesAccessor, channelId?: string): Promise { + return servicesAccessor.get(IInstantiationService).createInstance(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.TITLE.value).run(channelId); } }); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 519fab7f93a..f295d14a03c 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -12,7 +12,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { dirname, basename, isEqual } from 'vs/base/common/resources'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IOutputService } from 'vs/workbench/services/output/common/output'; +import { IOutputChannelDescriptor, IOutputService } from 'vs/workbench/services/output/common/output'; import { extensionTelemetryLogChannelId, telemetryLogId } from 'vs/platform/telemetry/common/telemetryUtils'; import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; import { Codicon } from 'vs/base/common/codicons'; @@ -36,8 +36,8 @@ export class SetLogLevelAction extends Action { super(id, label); } - override async run(): Promise { - const logLevelOrChannel = await this.selectLogLevelOrChannel(); + override async run(channelId?: string): Promise { + const logLevelOrChannel = await this.getLogLevelOrChannel(channelId); if (logLevelOrChannel !== null) { if (isLogLevel(logLevelOrChannel)) { this.loggerService.setLogLevel(logLevelOrChannel); @@ -47,16 +47,19 @@ export class SetLogLevelAction extends Action { } } - private async selectLogLevelOrChannel(): Promise { + private async getLogLevelOrChannel(channelId?: string): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!channel.log || !channel.file || channel.id === telemetryLogId || channel.id === extensionTelemetryLogChannelId) { + if (!SetLogLevelAction.isLevelSettable(channel) || !channel.file) { continue; } const channelLogLevel = this.loggerService.getLogLevel(channel.file) ?? logLevel; const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.file, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; + if (channelId && channel.id === channelId) { + return item; + } if (channel.extensionId) { extensionLogs.push(item); } else { @@ -96,6 +99,10 @@ export class SetLogLevelAction extends Action { }); } + static isLevelSettable(channel: IOutputChannelDescriptor): boolean { + return channel.log && channel.file !== undefined && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + } + private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 794a8e60d57..6ef9f9764ea 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -30,6 +30,8 @@ import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -99,6 +101,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerOpenActiveOutputFileInAuxWindowAction(); this.registerShowLogsAction(); this.registerOpenLogFileAction(); + this.registerConfigureActiveOutputLogLevelAction(); } private registerSwitchOutputAction(): void { @@ -334,6 +337,38 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { return null; } + private registerConfigureActiveOutputLogLevelAction(): void { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.configureActiveOutputLogLevel`, + title: nls.localize2('configureActiveOutputLogLevel', "Configure Log Level..."), + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: 'navigation', + order: 6, + isHiddenByDefault: true + }], + icon: Codicon.gear, + precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE + }); + } + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + that.configureActiveOutputLogLevel(commandService); + } + })); + } + + private async configureActiveOutputLogLevel(commandService: ICommandService): Promise { + const channel = this.outputService.getActiveChannel(); + if (channel) { + await commandService.executeCommand(SetLogLevelAction.ID, channel.id); + } + } + private registerShowLogsAction(): void { this._register(registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 26fed6ffc4e..e004aaa86bc 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,7 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; @@ -21,6 +21,7 @@ import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IOutputChannelModelService } from 'vs/workbench/contrib/output/common/outputChannelModelService'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -74,6 +75,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelContext: IContextKey; private readonly activeFileOutputChannelContext: IContextKey; + private readonly activeOutputChannelLevelSettableContext: IContextKey; constructor( @IStorageService private readonly storageService: IStorageService, @@ -91,6 +93,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel))); this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService); + this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService); // Register as text model content provider for output textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); @@ -196,7 +199,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; - this.activeFileOutputChannelContext.set(!!channel?.outputChannelDescriptor?.file); + const descriptor = channel?.outputChannelDescriptor; + this.activeFileOutputChannelContext.set(!!descriptor?.file); + this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); if (this.activeChannel) { this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 012855aaaee..716bb3bcfd6 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -43,6 +43,8 @@ export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); export const CONTEXT_ACTIVE_FILE_OUTPUT = new RawContextKey('activeLogOutput', false); +export const CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE = new RawContextKey('activeLogOutput.levelSettable', false); + export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); export const IOutputService = createDecorator('outputService'); From 2c08528d7ccce38528b26bc65119b77b26ed67da Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 14:21:46 -0800 Subject: [PATCH 003/141] #199119 --- .../browser/accessibilityConfiguration.ts | 2 + .../accessibility/browser/accessibleView.ts | 54 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 33a1379d3bd..bf0c372877a 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -22,6 +22,8 @@ export const accessibleViewVerbosityEnabled = new RawContextKey('access export const accessibleViewGoToSymbolSupported = new RawContextKey('accessibleViewGoToSymbolSupported', false, true); export const accessibleViewOnLastLine = new RawContextKey('accessibleViewOnLastLine', false, true); export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); +export const accessibleViewInCodeBlock = new RawContextKey('accessibleViewInCodeBlock', undefined, undefined); +export const accessibleViewContainsCodeBlocks = new RawContextKey('accessibleViewContainsCodeBlocks', undefined, undefined); /** * Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 07fccadd92b..8c59b8fd4cd 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -40,7 +40,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -127,6 +127,12 @@ export interface IAccessibleViewOptions { id?: AccessibleViewProviderId; } +interface ICodeBlock { + startLine: number; + endLine: number; + code: string; +} + export class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; @@ -137,6 +143,9 @@ export class AccessibleView extends Disposable { private _accessibleViewVerbosityEnabled: IContextKey; private _accessibleViewGoToSymbolSupported: IContextKey; private _accessibleViewCurrentProviderId: IContextKey; + private _accessibleViewInCodeBlock: IContextKey; + private _accessibleViewContainsCodeBlocks: IContextKey; + private _codeBlocks?: ICodeBlock[]; get editorWidget() { return this._editorWidget; } private _container: HTMLElement; @@ -169,6 +178,8 @@ export class AccessibleView extends Disposable { this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService); this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService); this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService); + this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService); + this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService); this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService); this._container = document.createElement('div'); @@ -229,6 +240,13 @@ export class AccessibleView extends Disposable { this._register(this._editorWidget.onDidChangeCursorPosition(() => { this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount()); })); + this._register(this._editorWidget.onDidChangeCursorPosition(() => { + const cursorPosition = this._editorWidget.getPosition()?.lineNumber; + if (this._codeBlocks && cursorPosition !== undefined) { + const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined; + this._accessibleViewInCodeBlock.set(inCodeBlock); + } + })); } private _resetContextKeys(): void { @@ -328,6 +346,33 @@ export class AccessibleView extends Disposable { this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider); } + calculateCodeBlocks(markdown: string): void { + if (!this._currentProvider) { + return; + } + if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { + // Symbols haven't been provided and we cannot parse this language + return; + } + const lines = markdown.split('\n'); + this._codeBlocks = []; + let inBlock = false; + let startLine = 0; + + lines.forEach((line, i) => { + if (!inBlock && line.startsWith('```')) { + inBlock = true; + startLine = i + 1; + } else if (inBlock && line.startsWith('```')) { + inBlock = false; + const endLine = i; + const code = lines.slice(startLine, endLine).join('\n'); + this._codeBlocks?.push({ startLine, endLine, code }); + } + }); + this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0); + } + getSymbols(): IAccessibleViewSymbol[] | undefined { if (!this._currentProvider || !this._currentContent) { return; @@ -459,7 +504,11 @@ export class AccessibleView extends Disposable { } const verbose = this._configurationService.getValue(provider.verbositySettingKey); const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + if (newContent && newContent !== this._currentContent && provider.options.type !== AccessibleViewType.Help && !provider.options.language || provider.options.language === 'markdown') { + this.calculateCodeBlocks(newContent); + } + this._currentContent = newContent; this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -775,6 +824,7 @@ export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { markdownToParse?: string; firstListItem?: string; lineNumber?: number; + endLineNumber?: number; } function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean { From 5ee040ec3a8381d4aaef2458c64a41c9e122df99 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Thu, 29 Feb 2024 02:34:52 +0000 Subject: [PATCH 004/141] Use action submenu instead of existing quickpick --- src/vs/platform/log/common/log.ts | 13 +++ .../contrib/logs/common/defaultLogLevels.ts | 27 +++++- .../contrib/logs/common/logs.contribution.ts | 4 +- .../contrib/logs/common/logsActions.ts | 21 +--- .../output/browser/output.contribution.ts | 97 +++++++++++++------ .../contrib/output/browser/outputServices.ts | 34 ++++++- .../services/output/common/output.ts | 4 + 7 files changed, 148 insertions(+), 52 deletions(-) diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index e2d69c0fe30..b0342da2653 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; @@ -12,6 +13,7 @@ import { isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import { Mutable, isNumber, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { ILocalizedString } from 'vs/platform/action/common/action'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -745,6 +747,17 @@ export function LogLevelToString(logLevel: LogLevel): string { } } +export function LogLevelToLocalizedString(logLevel: LogLevel): ILocalizedString { + switch (logLevel) { + case LogLevel.Trace: return { original: 'trace', value: nls.localize('trace', "Trace") }; + case LogLevel.Debug: return { original: 'debug', value: nls.localize('debug', "Debug") }; + case LogLevel.Info: return { original: 'info', value: nls.localize('info', "Info") }; + case LogLevel.Warning: return { original: 'warn', value: nls.localize('warn', "Warning") }; + case LogLevel.Error: return { original: 'error', value: nls.localize('error', "Error") }; + case LogLevel.Off: return { original: 'off', value: nls.localize('off', "Off") }; + } +} + export function parseLogLevel(logLevel: string): LogLevel | undefined { switch (logLevel) { case 'trace': diff --git a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts index 9feccec6965..522b64a8cd6 100644 --- a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts @@ -12,6 +12,8 @@ import { isString, isUndefined } from 'vs/base/common/types'; import { EXTENSION_IDENTIFIER_WITH_LOG_REGEX } from 'vs/platform/environment/common/environmentService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { parse } from 'vs/base/common/json'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; interface ParsedArgvLogLevels { default?: LogLevel; @@ -26,17 +28,27 @@ export interface IDefaultLogLevelsService { readonly _serviceBrand: undefined; + /** + * An event which fires when default log levels are changed + */ + readonly onDidChangeDefaultLogLevels: Event; + getDefaultLogLevels(): Promise; + getDefaultLogLevel(extensionId?: string): Promise; + setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise; migrateLogLevels(): void; } -class DefaultLogLevelsService implements IDefaultLogLevelsService { +class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsService { _serviceBrand: undefined; + private _onDidChangeDefaultLogLevels = this._register(new Emitter); + readonly onDidChangeDefaultLogLevels = this._onDidChangeDefaultLogLevels.event; + constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @@ -44,6 +56,7 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, ) { + super(); } async getDefaultLogLevels(): Promise { @@ -54,11 +67,20 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { }; } + async getDefaultLogLevel(extensionId?: string): Promise { + const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; + if (extensionId) { + extensionId = extensionId.toLowerCase(); + return this._getDefaultLogLevel(argvLogLevel, extensionId); + } else { + return this._getDefaultLogLevel(argvLogLevel); + } + } + async setDefaultLogLevel(defaultLogLevel: LogLevel, extensionId?: string): Promise { const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; if (extensionId) { extensionId = extensionId.toLowerCase(); - const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; const currentDefaultLogLevel = this._getDefaultLogLevel(argvLogLevel, extensionId); argvLogLevel.extensions = argvLogLevel.extensions ?? []; const extension = argvLogLevel.extensions.find(([extension]) => extension === extensionId); @@ -82,6 +104,7 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { this.loggerService.setLogLevel(defaultLogLevel); } } + this._onDidChangeDefaultLogLevels.fire(); } private _getDefaultLogLevel(argvLogLevels: ParsedArgvLogLevels, extension?: string): LogLevel { diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 2b95a4341ce..19257411641 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -36,8 +36,8 @@ registerAction2(class extends Action2 { f1: true }); } - run(servicesAccessor: ServicesAccessor, channelId?: string): Promise { - return servicesAccessor.get(IInstantiationService).createInstance(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.TITLE.value).run(channelId); + run(servicesAccessor: ServicesAccessor): Promise { + return servicesAccessor.get(IInstantiationService).createInstance(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.TITLE.value).run(); } }); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index f295d14a03c..3ececbb1223 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; -import { ILoggerService, LogLevel, isLogLevel } from 'vs/platform/log/common/log'; +import { ILoggerService, LogLevel, LogLevelToLocalizedString, isLogLevel } from 'vs/platform/log/common/log'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; @@ -36,8 +36,8 @@ export class SetLogLevelAction extends Action { super(id, label); } - override async run(channelId?: string): Promise { - const logLevelOrChannel = await this.getLogLevelOrChannel(channelId); + override async run(): Promise { + const logLevelOrChannel = await this.selectLogLevelOrChannel(); if (logLevelOrChannel !== null) { if (isLogLevel(logLevelOrChannel)) { this.loggerService.setLogLevel(logLevelOrChannel); @@ -47,7 +47,7 @@ export class SetLogLevelAction extends Action { } } - private async getLogLevelOrChannel(channelId?: string): Promise { + private async selectLogLevelOrChannel(): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); @@ -57,9 +57,6 @@ export class SetLogLevelAction extends Action { } const channelLogLevel = this.loggerService.getLogLevel(channel.file) ?? logLevel; const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.file, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; - if (channelId && channel.id === channelId) { - return item; - } if (channel.extensionId) { extensionLogs.push(item); } else { @@ -148,15 +145,7 @@ export class SetLogLevelAction extends Action { } private getLabel(level: LogLevel, current?: LogLevel): string { - let label: string; - switch (level) { - case LogLevel.Trace: label = nls.localize('trace', "Trace"); break; - case LogLevel.Debug: label = nls.localize('debug', "Debug"); break; - case LogLevel.Info: label = nls.localize('info', "Info"); break; - case LogLevel.Warning: label = nls.localize('warn', "Warning"); break; - case LogLevel.Error: label = nls.localize('err', "Error"); break; - case LogLevel.Off: label = nls.localize('off', "Off"); break; - } + const label = LogLevelToLocalizedString(level).value; return level === current ? `$(check) ${label}` : label; } diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 4133ae522b2..595c458f2f4 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -30,8 +30,8 @@ import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { ILoggerService, LogLevel, LogLevelToLocalizedString } from 'vs/platform/log/common/log'; +import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -339,34 +339,71 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { private registerConfigureActiveOutputLogLevelAction(): void { const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.action.configureActiveOutputLogLevel`, - title: nls.localize2('configureActiveOutputLogLevel', "Configure Log Level..."), - menu: [{ - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), - group: 'navigation', - order: 6, - isHiddenByDefault: true - }], - icon: Codicon.gear, - precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE - }); - } - async run(accessor: ServicesAccessor): Promise { - const commandService = accessor.get(ICommandService); - that.configureActiveOutputLogLevel(commandService); - } + const logLevelMenu = new MenuId('workbench.output.menu.logLevel'); + this._register(MenuRegistry.appendMenuItem(MenuId.ViewTitle, { + submenu: logLevelMenu, + title: nls.localize('logLevel.label', "Set Log Level..."), + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE), + icon: Codicon.gear, + order: 6 })); - } - - private async configureActiveOutputLogLevel(commandService: ICommandService): Promise { - const channel = this.outputService.getActiveChannel(); - if (channel) { - await commandService.executeCommand(SetLogLevelAction.ID, channel.id); - } + const registeredLogLevels = new Map(); + this._register(toDisposable(() => dispose(registeredLogLevels.values()))); + const registerLogLevels = (logLevels: LogLevel[]) => { + let order = 0; + for (const logLevel of logLevels) { + const { original, value } = LogLevelToLocalizedString(logLevel); + registeredLogLevels.set(logLevel, registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.output.activeOutputLogLevel.${logLevel}`, + title: value, + toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(original), + menu: { + id: logLevelMenu, + order: order++, + group: '0_level' + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const channel = that.outputService.getActiveChannel(); + if (channel) { + const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); + if (channelDescriptor?.log && channelDescriptor.file) { + return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); + } + } + } + })); + } + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.output.activeOutputLogLevelDefault`, + title: nls.localize('logLevelDefault.label', "Set As Default"), + menu: { + id: logLevelMenu, + order, + group: '1_default' + }, + precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.negate() + }); + } + async run(accessor: ServicesAccessor): Promise { + const channel = that.outputService.getActiveChannel(); + if (channel) { + const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); + if (channelDescriptor?.log && channelDescriptor.file) { + const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); + return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + } + } + } + })); + }; + registerLogLevels([LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Off]); } private registerShowLogsAction(): void { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index e004aaa86bc..718be38cf42 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,11 +9,11 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, ILoggerService, LogLevelToString } from 'vs/platform/log/common/log'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IOutputChannelModel } from 'vs/workbench/contrib/output/common/outputChannelModel'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -22,6 +22,7 @@ import { IOutputChannelModelService } from 'vs/workbench/contrib/output/common/o import { ILanguageService } from 'vs/editor/common/languages/language'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -76,15 +77,19 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelContext: IContextKey; private readonly activeFileOutputChannelContext: IContextKey; private readonly activeOutputChannelLevelSettableContext: IContextKey; + private readonly activeOutputChannelLevelContext: IContextKey; + private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; constructor( @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextModelService textModelResolverService: ITextModelService, @ILogService private readonly logService: ILogService, + @ILoggerService private readonly loggerService: ILoggerService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IViewsService private readonly viewsService: IViewsService, @IContextKeyService contextKeyService: IContextKeyService, + @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService ) { super(); this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, ''); @@ -94,6 +99,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService); this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService); + this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService); + this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); // Register as text model content provider for output textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); @@ -202,6 +209,29 @@ export class OutputService extends Disposable implements IOutputService, ITextMo const descriptor = channel?.outputChannelDescriptor; this.activeFileOutputChannelContext.set(!!descriptor?.file); this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); + const setLevelContext = (): void => { + const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; + this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); + }; + const setLevelIsDefaultContext = async (): Promise => { + const descriptor = this.activeChannel?.outputChannelDescriptor; + if (descriptor?.log) { + const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); + const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); + this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); + } else { + this.activeOutputChannelLevelIsDefaultContext.set(false); + } + }; + this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => { + setLevelIsDefaultContext(); + })); + setLevelIsDefaultContext(); + this._register(this.loggerService.onDidChangeLogLevel(_level => { + setLevelContext(); + setLevelIsDefaultContext(); + })); + setLevelContext(); if (this.activeChannel) { this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 716bb3bcfd6..25ec20fe32d 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -45,6 +45,10 @@ export const CONTEXT_ACTIVE_FILE_OUTPUT = new RawContextKey('activeLogO export const CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE = new RawContextKey('activeLogOutput.levelSettable', false); +export const CONTEXT_ACTIVE_OUTPUT_LEVEL = new RawContextKey('activeLogOutput.level', ''); + +export const CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT = new RawContextKey('activeLogOutput.levelIsDefault', false); + export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); export const IOutputService = createDecorator('outputService'); From f02db619a48dcb301f263f19faed87e8d92ed8f7 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 4 Mar 2024 17:56:08 +0100 Subject: [PATCH 005/141] Added footer area to composite parts and enabled activity bar positioning at the bottom --- src/vs/workbench/browser/layout.ts | 10 +- src/vs/workbench/browser/media/part.css | 9 +- src/vs/workbench/browser/part.ts | 31 ++++- .../parts/activitybar/activitybarPart.ts | 32 ++++- .../parts/auxiliarybar/auxiliaryBarPart.ts | 32 ++++- .../workbench/browser/parts/compositePart.ts | 8 ++ .../browser/parts/media/compositepart.css | 1 + .../browser/parts/media/paneCompositePart.css | 116 +++++++++++----- .../browser/parts/paneCompositePart.ts | 126 +++++++++++++++--- .../browser/parts/panel/panelPart.ts | 6 +- .../browser/parts/sidebar/sidebarPart.ts | 19 ++- .../browser/parts/titlebar/titlebarActions.ts | 3 +- .../browser/parts/titlebar/titlebarPart.ts | 3 +- .../browser/workbench.contribution.ts | 3 +- .../services/layout/browser/layoutService.ts | 4 +- 15 files changed, 329 insertions(+), 74 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 797a1713dd8..4eac33fbb1a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -359,9 +359,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE, ].some(setting => e.affectsConfiguration(setting))) { // Show Custom TitleBar if actions moved to the titlebar - const activityBarMovedToTop = e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION) && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; const editorActionsMovedToTitlebar = e.affectsConfiguration(LayoutSettings.EDITOR_ACTIONS_LOCATION) && this.configurationService.getValue(LayoutSettings.EDITOR_ACTIONS_LOCATION) === EditorActionsLocation.TITLEBAR; - if (activityBarMovedToTop || editorActionsMovedToTitlebar) { + + let activityBarMovedToTopOrBottom = false; + if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + activityBarMovedToTopOrBottom = activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM; + } + + if (activityBarMovedToTopOrBottom || editorActionsMovedToTitlebar) { if (this.configurationService.getValue(TitleBarSetting.CUSTOM_TITLE_BAR_VISIBILITY) === CustomTitleBarVisibility.NEVER) { this.configurationService.updateValue(TitleBarSetting.CUSTOM_TITLE_BAR_VISIBILITY, CustomTitleBarVisibility.AUTO); } diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 17182b6fa91..c9e200132b9 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -22,18 +22,21 @@ z-index: 12; } -.monaco-workbench .part > .title { +.monaco-workbench .part > .title, +.monaco-workbench .part > .footer { display: none; /* Parts have to opt in to show title area */ } -.monaco-workbench .part > .title { +.monaco-workbench .part > .title, +.monaco-workbench .part > .footer { height: 35px; display: flex; box-sizing: border-box; overflow: hidden; } -.monaco-workbench .part > .title { +.monaco-workbench .part > .title, +.monaco-workbench .part > .footer { padding-left: 8px; padding-right: 8px; } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 7ae271c65cc..0fe2060e6a4 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -16,12 +16,14 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IPartOptions { readonly hasTitle?: boolean; + readonly hasFooter?: () => boolean; readonly borderWidth?: () => number; } export interface ILayoutContentResult { readonly titleSize: IDimension; readonly contentSize: IDimension; + readonly footerSize: IDimension; } /** @@ -39,6 +41,7 @@ export abstract class Part extends Component implements ISerializableView { private parent: HTMLElement | undefined; private titleArea: HTMLElement | undefined; private contentArea: HTMLElement | undefined; + private footerArea: HTMLElement | undefined; private partLayout: PartLayout | undefined; constructor( @@ -75,6 +78,7 @@ export abstract class Part extends Component implements ISerializableView { this.parent = parent; this.titleArea = this.createTitleArea(parent, options); this.contentArea = this.createContentArea(parent, options); + this.footerArea = this.createFooterArea(parent, options); this.partLayout = new PartLayout(this.options, this.contentArea); @@ -116,6 +120,20 @@ export abstract class Part extends Component implements ISerializableView { return this.contentArea; } + /** + * Subclasses override to provide a footer area implementation. + */ + protected createFooterArea(parent: HTMLElement, options?: object): HTMLElement | undefined { + return undefined; + } + + /** + * Returns the footer area container. + */ + protected getFooterArea(): HTMLElement | undefined { + return this.footerArea; + } + /** * Layout title and content area in the given dimension. */ @@ -153,6 +171,7 @@ export abstract class Part extends Component implements ISerializableView { class PartLayout { private static readonly TITLE_HEIGHT = 35; + private static readonly FOOTER_HEIGHT = 35; constructor(private options: IPartOptions, private contentArea: HTMLElement | undefined) { } @@ -166,20 +185,28 @@ class PartLayout { titleSize = Dimension.None; } + // Footer Size: Width (Fill), Height (Variable) + let footerSize: Dimension; + if (this.options.hasFooter?.()) { + footerSize = new Dimension(width, Math.min(height, PartLayout.FOOTER_HEIGHT)); + } else { + footerSize = Dimension.None; + } + let contentWidth = width; if (this.options && typeof this.options.borderWidth === 'function') { contentWidth -= this.options.borderWidth(); // adjust for border size } // Content Size: Width (Fill), Height (Variable) - const contentSize = new Dimension(contentWidth, height - titleSize.height); + const contentSize = new Dimension(contentWidth, height - titleSize.height - footerSize.height); // Content if (this.contentArea) { size(this.contentArea, contentSize.width, contentSize.height); } - return { titleSize, contentSize }; + return { titleSize, contentSize, footerSize }; } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 2e2047e8f1d..2fc1c87d42f 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -388,7 +388,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 1 + order: 2 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), @@ -414,7 +414,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 2 + order: 1 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP), @@ -427,6 +427,32 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.activityBarLocation.bottom', + title: { + ...localize2('positionActivityBarBottom', 'Move Activity Bar to Bottom'), + mnemonicTitle: localize({ key: 'miBottomActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Bottom"), + }, + shortTitle: localize('bottom', "Bottom"), + category: Categories.View, + toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.BOTTOM), + menu: [{ + id: MenuId.ActivityBarPositionMenu, + order: 3 + }, { + id: MenuId.CommandPalette, + when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.BOTTOM), + }] + }); + } + run(accessor: ServicesAccessor): void { + const configurationService = accessor.get(IConfigurationService); + configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.BOTTOM); + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -440,7 +466,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.HIDDEN), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 3 + order: 4 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.HIDDEN), diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 5ad1c648d6f..777b1586efe 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -17,7 +17,7 @@ import { ActiveAuxiliaryContext, AuxiliaryBarFocusContext } from 'vs/workbench/c import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; @@ -29,6 +29,7 @@ import { AbstractPaneCompositePart } from 'vs/workbench/browser/parts/paneCompos import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { @@ -79,11 +80,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { @IExtensionService extensionService: IExtensionService, @ICommandService private commandService: ICommandService, @IMenuService menuService: IMenuService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super( Parts.AUXILIARYBAR_PART, { hasTitle: true, + hasFooter: () => this.shouldShowFooterCompositeBar(), borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, }, AuxiliaryBarPart.activePanelSettingsKey, @@ -104,6 +107,23 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { extensionService, menuService, ); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { + this.onDidChangeActivityBarLocation(); + } + })); + } + + private onDidChangeActivityBarLocation(): void { + this.removeCompositeBar(); + this.updateTitleArea(); + this.updateFooterArea(); + + const id = this.getActiveComposite()?.getId(); + if (id) { + this.onTitleAreaUpdate(id); + } } override updateStyles(): void { @@ -136,7 +156,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => HoverPosition.BELOW, + position: () => this.shouldShowFooterCompositeBar() ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => this.fillExtraContextMenuActions(actions), compositeSize: 0, @@ -170,8 +190,12 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { ]); } - protected shouldShowCompositeBar(): boolean { - return true; + protected shouldShowTitleCompositeBar(): boolean { + return !this.shouldShowFooterCompositeBar(); + } + + protected shouldShowFooterCompositeBar(): boolean { + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.BOTTOM; } override toJSON(): object { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index f4a0bb4fe28..69dc973cad1 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -438,6 +438,14 @@ export abstract class CompositePart extends Part { }; } + protected override createFooterArea(parent: HTMLElement): HTMLElement { + const footerArea = append(parent, $('.composite')); + footerArea.classList.add('footer'); + + hide(footerArea); + return footerArea; + } + override updateStyles(): void { super.updateStyles(); diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index fde7cdb706c..9dc8e5b560f 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/src/vs/workbench/browser/parts/media/compositepart.css @@ -7,6 +7,7 @@ height: 100%; } +.monaco-workbench .part > .composite.footer, .monaco-workbench .part > .composite.title { display: flex; } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 9250cd44de1..b35ecf91e49 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,11 +20,17 @@ display: none; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container { +.monaco-workbench .pane-composite-part > .footer{ + border-top: 1px solid var(--vscode-sideBarSectionHeader-border); +} + +.monaco-workbench .pane-composite-part > .title > .composite-bar-container, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container { display: flex; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; align-items: center; justify-content: center; @@ -33,12 +39,14 @@ color: inherit !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { text-transform: uppercase; padding-left: 10px; padding-right: 10px; @@ -48,22 +56,27 @@ display: flex; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 24px; padding: 0 5px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { font-size: 18px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { width: 16px; height: 16px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { content: ''; width: 2px; height: 24px; @@ -77,26 +90,33 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { display: block; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { left: 1px; margin-left: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { right: 1px; margin-right: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { + +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { left: 2px; margin-left: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { right: 2px; margin-right: -2px; } @@ -104,7 +124,11 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { transition-delay: 0s; } @@ -112,39 +136,52 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { margin-right: 0; padding: 2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { border-radius: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { background: none !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { margin-right: 0; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { margin-left: 8px; display: flex; align-items: center; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { margin-left: 0px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { padding: 3px 5px; border-radius: 11px; font-size: 11px; @@ -158,7 +195,8 @@ position: relative; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; top: 0; bottom: 0; @@ -170,7 +208,8 @@ z-index: 2; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { position: absolute; top: 11px; right: 0px; @@ -184,7 +223,8 @@ text-align: center; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; -webkit-mask-size: 11px; top: 3px; @@ -192,7 +232,8 @@ } /* active item indicator */ -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { position: absolute; z-index: 1; bottom: 0; @@ -201,20 +242,24 @@ height: 100%; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { top: -4px; left: 10px; width: calc(100% - 20px); } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { top: 1px; left: 2px; width: calc(100% - 4px); } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { content: ""; position: absolute; z-index: 1; @@ -225,20 +270,25 @@ border-top-style: solid; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { border-top-color: transparent !important; /* hides border on clicked state */ } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, +.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 3a1cb5382e4..8010742b79a 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -15,7 +15,7 @@ import { IView } from 'vs/base/browser/ui/grid/grid'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { CompositePart, ICompositeTitleLabel } from 'vs/workbench/browser/parts/compositePart'; import { IPaneCompositeBarOptions, PaneCompositeBar } from 'vs/workbench/browser/parts/paneCompositeBar'; -import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend, getWindow } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend, getWindow, hide, show, IDomPosition } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -108,8 +108,12 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); + private titleCompositeBarVisible: boolean = false; + private footerCompositeBarVisible: boolean = false; private emptyPaneMessageElement: HTMLElement | undefined; private globalToolBar: ToolBar | undefined; @@ -117,6 +121,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart { + this.onFooterAreaContextMenu(new StandardMouseEvent(getWindow(this.footerContainer), e)); + })); + this.footerDispoables.add(Gesture.addTarget(this.footerContainer)); + this.footerDispoables.add(addDisposableListener(this.footerContainer, GestureEventType.Contextmenu, e => { + this.onFooterAreaContextMenu(new StandardMouseEvent(getWindow(this.footerContainer), e)); + })); + } else { + hide(this.footerContainer); + + // Dispose any footer area listener + this.footerDispoables.clear(); + } + + if (this.contentDimension && this.contentPosition) { + this.layout(this.contentDimension.width, this.contentDimension.height, this.contentPosition.top, this.contentPosition.left); + } + } + + protected createCompositeBar(): PaneCompositeBar { return this.instantiationService.createInstance(PaneCompositeBar, this.getCompositeBarOptions(), this.partId, this); } @@ -444,6 +518,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart event, - getActions: () => actions, - skipTelemetry: true - }); - } + if (this.shouldShowTitleCompositeBar()) { + return this.onCompositeBarContextMenu(event); } else { const activePaneComposite = this.getActivePaneComposite() as PaneComposite; const activePaneCompositeActions = activePaneComposite ? activePaneComposite.getContextMenuActions() : []; @@ -512,6 +580,25 @@ export abstract class AbstractPaneCompositePart extends CompositePart event, + getActions: () => actions, + skipTelemetry: true + }); + } + } + } + protected getViewsSubmenuAction(): SubmenuAction | undefined { const viewPaneContainer = (this.getActivePaneComposite() as PaneComposite)?.getViewPaneContainer(); if (viewPaneContainer) { @@ -527,7 +614,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }, + { hasTitle: true, hasFooter: () => this.shouldShowFooterCompositeBar(), borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }, SidebarPart.activeViewletSettingsKey, ActiveViewletContext.bindTo(contextKeyService), SidebarFocusContext.bindTo(contextKeyService), @@ -112,7 +112,10 @@ export class SidebarPart extends AbstractPaneCompositePart { } private onDidChangeActivityBarLocation(): void { + this.removeCompositeBar(); this.updateTitleArea(); + this.updateFooterArea(); + const id = this.getActiveComposite()?.getId(); if (id) { this.onTitleAreaUpdate(id); @@ -153,7 +156,7 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.layoutService.getSideBarPosition() === SideBarPosition.LEFT ? AnchorAlignment.LEFT : AnchorAlignment.RIGHT; } - protected override createCompisteBar(): ActivityBarCompositeBar { + protected override createCompositeBar(): ActivityBarCompositeBar { return this.instantiationService.createInstance(ActivityBarCompositeBar, this.getCompositeBarOptions(), this.partId, this, false); } @@ -167,7 +170,7 @@ export class SidebarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => HoverPosition.BELOW, + position: () => this.shouldShowFooterCompositeBar() ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => { const viewsSubmenuAction = this.getViewsSubmenuAction(); @@ -194,9 +197,18 @@ export class SidebarPart extends AbstractPaneCompositePart { } protected shouldShowCompositeBar(): boolean { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + return activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM; + } + + protected shouldShowTitleCompositeBar(): boolean { return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; } + protected shouldShowFooterCompositeBar(): boolean { + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.BOTTOM; + } + private shouldShowActivityBar(): boolean { if (this.shouldShowCompositeBar()) { return false; @@ -215,6 +227,7 @@ export class SidebarPart extends AbstractPaneCompositePart { const activityBarPosition = this.storageService.get(LayoutSettings.ACTIVITY_BAR_LOCATION, StorageScope.PROFILE); switch (activityBarPosition) { case ActivityBarPosition.SIDE: return ActivityBarPosition.SIDE; + case ActivityBarPosition.BOTTOM: return ActivityBarPosition.BOTTOM; default: return ActivityBarPosition.TOP; } } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index 8b9ec83840d..632bf55e2e2 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -116,7 +116,8 @@ class ToggleCustomTitleBar extends Action2 { ContextKeyExpr.equals('config.workbench.layoutControl.enabled', false), ContextKeyExpr.equals('config.window.commandCenter', false), ContextKeyExpr.notEquals('config.workbench.editor.editorActionsLocation', 'titleBar'), - ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'top') + ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'top'), + ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'bottom') )?.negate() ), IsMainWindowFullscreenContext diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 548f4d36caf..f07a3ef9858 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -728,7 +728,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } private get activityActionsEnabled(): boolean { - return !this.isAuxiliary && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + return !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM); } get hasZoomableElements(): boolean { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index b715436c0d4..fc4af190dca 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -497,12 +497,13 @@ const registry = Registry.as(ConfigurationExtensions.Con }, [LayoutSettings.ACTIVITY_BAR_LOCATION]: { 'type': 'string', - 'enum': ['side', 'top', 'hidden'], + 'enum': ['side', 'top', 'bottom', 'hidden'], 'default': 'side', 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `side` or `top` of the Primary Side Bar or `hidden`."), 'enumDescriptions': [ localize('workbench.activityBar.location.side', "Show the Activity Bar to the side of the Primary Side Bar."), localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary Side Bar."), + localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary Side Bar."), localize('workbench.activityBar.location.hide', "Hide the Activity Bar.") ], }, diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 0a397839220..b25b1a66c14 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -50,6 +50,7 @@ export const enum LayoutSettings { export const enum ActivityBarPosition { SIDE = 'side', TOP = 'top', + BOTTOM = 'bottom', HIDDEN = 'hidden' } @@ -371,7 +372,8 @@ function isTitleBarEmpty(configurationService: IConfigurationService): boolean { } // with the activity bar on top, we should always show - if (configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP) { + const activityBarPosition = configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + if (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM) { return false; } From 7c9b58252948d0e73619f9accf78b69441d9068d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 15:02:30 -0800 Subject: [PATCH 006/141] get all actions to work --- .../accessibility/browser/accessibleView.ts | 22 +++++++- .../browser/actions/chatCodeblockActions.ts | 52 ++++++++++++------- .../contrib/chat/browser/codeBlockPart.ts | 2 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 8c59b8fd4cd..294cc709daf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -42,6 +42,7 @@ import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQui import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; const enum DIMENSIONS { @@ -91,6 +92,7 @@ export interface IAccessibleViewService { * @param verbositySettingKey The setting key for the verbosity of the feature */ getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; + getCodeBlockContext(): ICodeBlockActionContext | undefined; } export const enum AccessibleViewType { @@ -131,6 +133,7 @@ interface ICodeBlock { startLine: number; endLine: number; code: string; + languageId?: string; } export class AccessibleView extends Disposable { @@ -272,6 +275,18 @@ export class AccessibleView extends Disposable { } } + getCodeBlockContext(): ICodeBlockActionContext | undefined { + const position = this._editorWidget.getPosition(); + if (!this._codeBlocks?.length || !position) { + return; + } + const codeBlockIndex = this._codeBlocks?.findIndex(c => c.startLine <= position?.lineNumber && c.endLine >= position?.lineNumber); + const codeBlock = codeBlockIndex !== undefined && codeBlockIndex > -1 ? this._codeBlocks[codeBlockIndex] : undefined; + if (!codeBlock || codeBlockIndex === undefined) { + return; + } + return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined }; + } showLastProvider(id: AccessibleViewProviderId): void { if (!this._lastProvider || this._lastProvider.options.id !== id) { @@ -360,14 +375,16 @@ export class AccessibleView extends Disposable { let startLine = 0; lines.forEach((line, i) => { + let languageId: string | undefined; if (!inBlock && line.startsWith('```')) { inBlock = true; startLine = i + 1; + languageId = line.substring(3).trim(); } else if (inBlock && line.startsWith('```')) { inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); - this._codeBlocks?.push({ startLine, endLine, code }); + this._codeBlocks?.push({ startLine, endLine, code, languageId }); } }); this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0); @@ -783,6 +800,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView editorWidget?.revealLine(position.lineNumber); } } + getCodeBlockContext(): ICodeBlockActionContext | undefined { + return this._accessibleView?.getCodeBlockContext(); + } } class AccessibleViewSymbolQuickPick { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 27f5c9f07f8..0db49de0bf8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -24,6 +24,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -93,7 +95,11 @@ export function registerChatCodeBlockActions() { } run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; + const accessibleViewService = accessor.get(IAccessibleViewService); + let context = args[0]; + if (!context) { + context = accessibleViewService.getCodeBlockContext(); + } if (!isCodeBlockActionContext(context) || isResponseFiltered(context)) { return; } @@ -147,21 +153,24 @@ export function registerChatCodeBlockActions() { // Report copy to extensions const chatService = accessor.get(IChatService); - chatService.notifyUserAction({ - providerId: context.element.providerId, - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'copy', - codeBlockIndex: context.codeBlockIndex, - copyKind: ChatCopyKind.Action, - copiedText, - copiedCharacters: copiedText.length, - totalCharacters, - } - }); + const element = context.element as IChatResponseViewModel | undefined; + if (element) { + chatService.notifyUserAction({ + providerId: element.providerId, + agentId: element.agent?.id, + sessionId: element.sessionId, + requestId: element.requestId, + result: element.result, + action: { + kind: 'copy', + codeBlockIndex: context.codeBlockIndex, + copyKind: ChatCopyKind.Action, + copiedText, + copiedCharacters: copiedText.length, + totalCharacters, + } + }); + } // Copy full cell if no selection, otherwise fall back on normal editor implementation if (noSelection) { @@ -187,7 +196,7 @@ export function registerChatCodeBlockActions() { when: CONTEXT_IN_CHAT_SESSION, }, keybinding: { - when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), + when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, weight: KeybindingWeight.WorkbenchContrib @@ -431,7 +440,7 @@ export function registerChatCodeBlockActions() { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter }, weight: KeybindingWeight.EditorContrib, - when: CONTEXT_IN_CHAT_SESSION, + when: ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, accessibleViewInCodeBlock), }] }); } @@ -557,8 +566,13 @@ export function registerChatCodeBlockActions() { }); } -function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): IChatCodeBlockActionContext | undefined { +function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); + const accessibleViewService = accessor.get(IAccessibleViewService); + const accessibleViewCodeBlock = accessibleViewService.getCodeBlockContext(); + if (accessibleViewCodeBlock) { + return accessibleViewCodeBlock; + } const model = editor.getModel(); if (!model) { return; diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index b6d4500e448..d1531d7db44 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -101,8 +101,8 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; - languageId: string; codeBlockIndex: number; + languageId?: string; element: unknown; } From 5f511caaaf94354770a9e74ca9daa408c4a8ee62 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 15:05:26 -0800 Subject: [PATCH 007/141] reorder --- src/vs/workbench/contrib/chat/browser/codeBlockPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index d1531d7db44..a39f69dbac8 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -101,8 +101,8 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; - codeBlockIndex: number; languageId?: string; + codeBlockIndex: number; element: unknown; } From 65cfaf8ad1e1f5a58e5a8d11b8ed619b7a10da44 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 5 Mar 2024 11:51:22 +0100 Subject: [PATCH 008/141] css --- .../auxiliarybar/media/auxiliaryBarPart.css | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 3d382b3e39d..ad3e53ded5a 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -14,6 +14,22 @@ background-color: var(--vscode-sideBar-background); } +.monaco-workbench .part.auxiliarybar .title-actions .actions-container { + justify-content: flex-end; +} + +.monaco-workbench .part.auxiliarybar .title-actions .action-item { + margin-right: 4px; +} + +.monaco-workbench .part.auxiliarybar > .title > .title-label { + flex: 1; +} + +.monaco-workbench .part.auxiliarybar > .title > .title-label h2 { + text-transform: uppercase; +} + .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container { flex: 1; } @@ -37,7 +53,7 @@ outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } -.monaco-workbench .auxiliarybar.part.pane-composite-part > .composite.title.has-composite-bar > .title-actions { +.monaco-workbench .auxiliarybar.part.pane-composite-part > .composite.title > .title-actions { flex: inherit; } From bb71ca5a59c66d882b5bf7509212990a99c02d14 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 Mar 2024 11:57:24 +0100 Subject: [PATCH 009/141] design 2 --- src/vs/workbench/browser/media/part.css | 8 +- src/vs/workbench/browser/part.ts | 75 ++++++--- .../parts/auxiliarybar/auxiliaryBarPart.ts | 36 ++-- .../workbench/browser/parts/compositePart.ts | 8 +- .../browser/parts/media/compositepart.css | 4 +- .../browser/parts/media/paneCompositePart.css | 93 +++++----- .../browser/parts/paneCompositePart.ts | 159 ++++++++---------- .../browser/parts/panel/panelPart.ts | 8 +- .../browser/parts/sidebar/sidebarPart.ts | 29 ++-- .../browser/parts/views/viewPaneContainer.ts | 10 +- 10 files changed, 226 insertions(+), 204 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index c9e200132b9..b299dfa5e4a 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -23,12 +23,12 @@ } .monaco-workbench .part > .title, -.monaco-workbench .part > .footer { - display: none; /* Parts have to opt in to show title area */ +.monaco-workbench .part > .composite-bar-area { + display: none; /* Parts have to opt in to show and composite bar area */ } .monaco-workbench .part > .title, -.monaco-workbench .part > .footer { +.monaco-workbench .part > .composite-bar-area { height: 35px; display: flex; box-sizing: border-box; @@ -36,7 +36,7 @@ } .monaco-workbench .part > .title, -.monaco-workbench .part > .footer { +.monaco-workbench .part > .composite-bar-area { padding-left: 8px; padding-right: 8px; } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 0fe2060e6a4..0141d6c2707 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/part'; import { Component } from 'vs/workbench/common/component'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, IDimension, getActiveDocument } from 'vs/base/browser/dom'; +import { Dimension, size, IDimension, getActiveDocument, prepend, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ISerializableView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { Event, Emitter } from 'vs/base/common/event'; @@ -16,14 +16,13 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IPartOptions { readonly hasTitle?: boolean; - readonly hasFooter?: () => boolean; readonly borderWidth?: () => number; } export interface ILayoutContentResult { readonly titleSize: IDimension; readonly contentSize: IDimension; - readonly footerSize: IDimension; + readonly compositeBarSize: IDimension; } /** @@ -35,13 +34,16 @@ export abstract class Part extends Component implements ISerializableView { private _dimension: Dimension | undefined; get dimension(): Dimension | undefined { return this._dimension; } + private _contentPosition: IDomPosition | undefined; + get contentPosition(): IDomPosition | undefined { return this._contentPosition; } + protected _onDidVisibilityChange = this._register(new Emitter()); readonly onDidVisibilityChange = this._onDidVisibilityChange.event; private parent: HTMLElement | undefined; private titleArea: HTMLElement | undefined; private contentArea: HTMLElement | undefined; - private footerArea: HTMLElement | undefined; + private compositeBarArea: HTMLElement | undefined; private partLayout: PartLayout | undefined; constructor( @@ -78,7 +80,6 @@ export abstract class Part extends Component implements ISerializableView { this.parent = parent; this.titleArea = this.createTitleArea(parent, options); this.contentArea = this.createContentArea(parent, options); - this.footerArea = this.createFooterArea(parent, options); this.partLayout = new PartLayout(this.options, this.contentArea); @@ -121,19 +122,44 @@ export abstract class Part extends Component implements ISerializableView { } /** - * Subclasses override to provide a footer area implementation. + * Creates the composite bar and sets the proper position. */ - protected createFooterArea(parent: HTMLElement, options?: object): HTMLElement | undefined { - return undefined; + protected setCompositeBarArea(compositeBarContainer: HTMLElement, position: 'top' | 'bottom'): void { + if (this.compositeBarArea) { + throw new Error('Composite bar already exists'); + } + + if (!this.parent || !this.titleArea) { + return; + } + + if (position === 'top') { + prepend(this.parent, compositeBarContainer); + compositeBarContainer.classList.add('top'); + } else { + this.parent.appendChild(compositeBarContainer); + compositeBarContainer.classList.add('bottom'); + } + + this.compositeBarArea = compositeBarContainer; + this.partLayout?.setCompositeBarVisibility(true); + this.relayout(); } - /** - * Returns the footer area container. - */ - protected getFooterArea(): HTMLElement | undefined { - return this.footerArea; + protected removeCompositeBarArea(): void { + if (this.compositeBarArea) { + this.compositeBarArea.remove(); + this.compositeBarArea = undefined; + this.partLayout?.setCompositeBarVisibility(false); + this.relayout(); + } } + private relayout() { + if (this.dimension && this.contentPosition) { + this.layout(this.dimension.width, this.dimension.height, this.contentPosition.top, this.contentPosition.left); + } + } /** * Layout title and content area in the given dimension. */ @@ -155,8 +181,9 @@ export abstract class Part extends Component implements ISerializableView { abstract minimumHeight: number; abstract maximumHeight: number; - layout(width: number, height: number, _top: number, _left: number): void { + layout(width: number, height: number, top: number, left: number): void { this._dimension = new Dimension(width, height); + this._contentPosition = { top, left }; } setVisible(visible: boolean) { @@ -171,7 +198,9 @@ export abstract class Part extends Component implements ISerializableView { class PartLayout { private static readonly TITLE_HEIGHT = 35; - private static readonly FOOTER_HEIGHT = 35; + private static readonly COMPOSITE_BAR_HEIGHT = 35; + + private compositeBarAreaVisible: boolean = false; constructor(private options: IPartOptions, private contentArea: HTMLElement | undefined) { } @@ -186,11 +215,11 @@ class PartLayout { } // Footer Size: Width (Fill), Height (Variable) - let footerSize: Dimension; - if (this.options.hasFooter?.()) { - footerSize = new Dimension(width, Math.min(height, PartLayout.FOOTER_HEIGHT)); + let compositeBarSize: Dimension; + if (this.compositeBarAreaVisible) { + compositeBarSize = new Dimension(width, Math.min(height, PartLayout.COMPOSITE_BAR_HEIGHT)); } else { - footerSize = Dimension.None; + compositeBarSize = Dimension.None; } let contentWidth = width; @@ -199,14 +228,18 @@ class PartLayout { } // Content Size: Width (Fill), Height (Variable) - const contentSize = new Dimension(contentWidth, height - titleSize.height - footerSize.height); + const contentSize = new Dimension(contentWidth, height - titleSize.height - compositeBarSize.height); // Content if (this.contentArea) { size(this.contentArea, contentSize.width, contentSize.height); } - return { titleSize, contentSize, footerSize }; + return { titleSize, contentSize, compositeBarSize }; + } + + setCompositeBarVisibility(visible: boolean): void { + this.compositeBarAreaVisible = visible; } } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 777b1586efe..e01dc69a69d 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -19,17 +19,18 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IAction, Separator, toAction } from 'vs/base/common/actions'; +import { IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { assertIsDefined } from 'vs/base/common/types'; import { LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; import { ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { AbstractPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/browser/parts/paneCompositePart'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; -import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { @@ -86,7 +87,6 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { Parts.AUXILIARYBAR_PART, { hasTitle: true, - hasFooter: () => this.shouldShowFooterCompositeBar(), borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, }, AuxiliaryBarPart.activePanelSettingsKey, @@ -116,9 +116,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { } private onDidChangeActivityBarLocation(): void { - this.removeCompositeBar(); - this.updateTitleArea(); - this.updateFooterArea(); + this.updateCompositeBar(); const id = this.getActiveComposite()?.getId(); if (id) { @@ -156,7 +154,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => this.shouldShowFooterCompositeBar() ? HoverPosition.ABOVE : HoverPosition.BELOW, + position: () => this.getCompositeBarPosition() === CompositeBarPosition.BOTTOM ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => this.fillExtraContextMenuActions(actions), compositeSize: 0, @@ -183,19 +181,33 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { actions.push(new Separator()); actions.push(viewsSubmenuAction); } + + const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const positionActions: IAction[] = []; + createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); + activityBarPositionMenu.dispose(); + actions.push(...[ new Separator(), + new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }), toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) }) ]); } - protected shouldShowTitleCompositeBar(): boolean { - return !this.shouldShowFooterCompositeBar(); + protected shouldShowCompositeBar(): boolean { + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; } - protected shouldShowFooterCompositeBar(): boolean { - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.BOTTOM; + protected getCompositeBarPosition(): CompositeBarPosition { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + switch (activityBarPosition) { + case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; + case ActivityBarPosition.HIDDEN: return CompositeBarPosition.TITLE; + case ActivityBarPosition.SIDE: return CompositeBarPosition.TITLE; + case ActivityBarPosition.TOP: + default: return CompositeBarPosition.TOP; + } } override toJSON(): object { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 69dc973cad1..146a58620b1 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -438,12 +438,8 @@ export abstract class CompositePart extends Part { }; } - protected override createFooterArea(parent: HTMLElement): HTMLElement { - const footerArea = append(parent, $('.composite')); - footerArea.classList.add('footer'); - - hide(footerArea); - return footerArea; + protected createCompositeBarArea(): HTMLElement { + return $('.composite.composite-bar-area'); } override updateStyles(): void { diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index 9dc8e5b560f..4f7afbfab50 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/src/vs/workbench/browser/parts/media/compositepart.css @@ -7,7 +7,7 @@ height: 100%; } -.monaco-workbench .part > .composite.footer, +.monaco-workbench .part > .composite.composite-bar-area, .monaco-workbench .part > .composite.title { display: flex; } @@ -15,4 +15,4 @@ .monaco-workbench .part > .composite.title > .title-actions { flex: 1; padding-left: 5px; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index b35ecf91e49..a324f08d4b4 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,17 +20,18 @@ display: none; } -.monaco-workbench .pane-composite-part > .footer{ +.monaco-workbench .pane-composite-part > .composite-bar-area.bottom { border-top: 1px solid var(--vscode-sideBarSectionHeader-border); } + .monaco-workbench .pane-composite-part > .title > .composite-bar-container, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container { display: flex; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; align-items: center; justify-content: center; @@ -40,13 +41,13 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { text-transform: uppercase; padding-left: 10px; padding-right: 10px; @@ -57,26 +58,26 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 24px; padding: 0 5px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { font-size: 18px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { width: 16px; height: 16px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { content: ''; width: 2px; height: 24px; @@ -91,32 +92,32 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { display: block; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { left: 1px; margin-left: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { right: 1px; margin-right: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { left: 2px; margin-left: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { right: 2px; margin-right: -2px; } @@ -125,10 +126,10 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { transition-delay: 0s; } @@ -137,51 +138,51 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { margin-right: 0; padding: 2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { border-radius: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { background: none !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { margin-right: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { margin-left: 8px; display: flex; align-items: center; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { margin-left: 0px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { padding: 3px 5px; border-radius: 11px; font-size: 11px; @@ -196,7 +197,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; top: 0; bottom: 0; @@ -209,7 +210,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { position: absolute; top: 11px; right: 0px; @@ -224,7 +225,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; -webkit-mask-size: 11px; top: 3px; @@ -233,7 +234,7 @@ /* active item indicator */ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { position: absolute; z-index: 1; bottom: 0; @@ -243,14 +244,14 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { top: -4px; left: 10px; width: calc(100% - 20px); } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { top: 1px; left: 2px; width: calc(100% - 4px); @@ -258,8 +259,8 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { content: ""; position: absolute; z-index: 1; @@ -271,24 +272,24 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { border-top-color: transparent !important; /* hides border on clicked state */ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, -.monaco-workbench .pane-composite-part > .footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { +.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 8010742b79a..876e85c3aea 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -15,7 +15,7 @@ import { IView } from 'vs/base/browser/ui/grid/grid'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { CompositePart, ICompositeTitleLabel } from 'vs/workbench/browser/parts/compositePart'; import { IPaneCompositeBarOptions, PaneCompositeBar } from 'vs/workbench/browser/parts/paneCompositeBar'; -import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend, getWindow, hide, show, IDomPosition } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend, getWindow } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -40,6 +40,12 @@ import { Composite } from 'vs/workbench/browser/composite'; import { ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +export enum CompositeBarPosition { + TOP, + TITLE, + BOTTOM +} + export interface IPaneCompositePart extends IView { readonly partId: Parts.PANEL_PART | Parts.AUXILIARYBAR_PART | Parts.SIDEBAR_PART; @@ -108,12 +114,11 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); - private titleCompositeBarVisible: boolean = false; - private footerCompositeBarVisible: boolean = false; + private compositeBarPosition: CompositeBarPosition | undefined = undefined; private emptyPaneMessageElement: HTMLElement | undefined; private globalToolBar: ToolBar | undefined; @@ -121,7 +126,6 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.paneFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.paneFocusContextKey.set(false))); @@ -331,96 +337,78 @@ export abstract class AbstractPaneCompositePart extends CompositePart { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); + })); + this.separateCompositeBarDispoables.add(Gesture.addTarget(compositeBarArea)); + this.separateCompositeBarDispoables.add(addDisposableListener(compositeBarArea, GestureEventType.Contextmenu, e => { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); + })); + + return compositeBarArea; } - private setFooterVisibility(visible: boolean): void { - if (!this.footerContainer) { - return; - } - - if (visible) { - show(this.footerContainer); - - // Only add listener if the footer is visible - this.footerDispoables.add(addDisposableListener(this.footerContainer, EventType.CONTEXT_MENU, e => { - this.onFooterAreaContextMenu(new StandardMouseEvent(getWindow(this.footerContainer), e)); - })); - this.footerDispoables.add(Gesture.addTarget(this.footerContainer)); - this.footerDispoables.add(addDisposableListener(this.footerContainer, GestureEventType.Contextmenu, e => { - this.onFooterAreaContextMenu(new StandardMouseEvent(getWindow(this.footerContainer), e)); - })); - } else { - hide(this.footerContainer); - - // Dispose any footer area listener - this.footerDispoables.clear(); - } - - if (this.contentDimension && this.contentPosition) { - this.layout(this.contentDimension.width, this.contentDimension.height, this.contentPosition.top, this.contentPosition.left); - } + protected override removeCompositeBarArea(): void { + this.separateCompositeBarContainer = undefined; + this.separateCompositeBarDispoables.clear(); + super.removeCompositeBarArea(); } protected createCompositeBar(): PaneCompositeBar { @@ -518,7 +506,6 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.shouldShowFooterCompositeBar(), borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }, + { hasTitle: true, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }, SidebarPart.activeViewletSettingsKey, ActiveViewletContext.bindTo(contextKeyService), SidebarFocusContext.bindTo(contextKeyService), @@ -112,9 +112,7 @@ export class SidebarPart extends AbstractPaneCompositePart { } private onDidChangeActivityBarLocation(): void { - this.removeCompositeBar(); - this.updateTitleArea(); - this.updateFooterArea(); + this.updateCompositeBar(); const id = this.getActiveComposite()?.getId(); if (id) { @@ -170,7 +168,7 @@ export class SidebarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => this.shouldShowFooterCompositeBar() ? HoverPosition.ABOVE : HoverPosition.BELOW, + position: () => this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.BOTTOM ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => { const viewsSubmenuAction = this.getViewsSubmenuAction(); @@ -201,14 +199,6 @@ export class SidebarPart extends AbstractPaneCompositePart { return activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM; } - protected shouldShowTitleCompositeBar(): boolean { - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; - } - - protected shouldShowFooterCompositeBar(): boolean { - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.BOTTOM; - } - private shouldShowActivityBar(): boolean { if (this.shouldShowCompositeBar()) { return false; @@ -216,6 +206,17 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; } + protected getCompositeBarPosition(): CompositeBarPosition { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + switch (activityBarPosition) { + case ActivityBarPosition.TOP: return CompositeBarPosition.TOP; + case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; + case ActivityBarPosition.HIDDEN: + case ActivityBarPosition.SIDE: + default: return CompositeBarPosition.TITLE; + } + } + private rememberActivityBarVisiblePosition(): void { const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); if (activityBarPosition !== ActivityBarPosition.HIDDEN) { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index f575fa95242..2b152911946 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -20,7 +20,7 @@ import * as nls from 'vs/nls'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Action2, IAction2Options, IMenuService, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -39,7 +39,7 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, LayoutSettings, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const ViewsSubMenu = new MenuId('Views'); @@ -47,7 +47,6 @@ MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { submenu: ViewsSubMenu, title: nls.localize('views', "Views"), order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP)), }); export interface IViewPaneContainerOptions extends IPaneViewOptions { @@ -1091,11 +1090,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } isViewMergedWithContainer(): boolean { - const location = this.viewDescriptorService.getViewContainerLocation(this.viewContainer); - // Do not merge views in side bar when activity bar is on top because the view title is not shown - if (location === ViewContainerLocation.Sidebar && !this.layoutService.isVisible(Parts.ACTIVITYBAR_PART) && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP) { - return false; - } if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } From ab3fbb5baa23595f47d0c8bd2ef05461116ebd78 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 Mar 2024 12:49:26 +0100 Subject: [PATCH 010/141] header / footer --- src/vs/workbench/browser/media/part.css | 8 +- src/vs/workbench/browser/part.ts | 110 +++++++++++++----- .../workbench/browser/parts/compositePart.ts | 2 +- .../browser/parts/media/compositepart.css | 2 +- .../browser/parts/media/paneCompositePart.css | 92 +++++++-------- .../browser/parts/paneCompositePart.ts | 44 ++++--- 6 files changed, 157 insertions(+), 101 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index b299dfa5e4a..f2787c2bf3b 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -23,12 +23,12 @@ } .monaco-workbench .part > .title, -.monaco-workbench .part > .composite-bar-area { - display: none; /* Parts have to opt in to show and composite bar area */ +.monaco-workbench .part > .header-or-footer { + display: none; /* Parts have to opt in to show area */ } .monaco-workbench .part > .title, -.monaco-workbench .part > .composite-bar-area { +.monaco-workbench .part > .header-or-footer { height: 35px; display: flex; box-sizing: border-box; @@ -36,7 +36,7 @@ } .monaco-workbench .part > .title, -.monaco-workbench .part > .composite-bar-area { +.monaco-workbench .part > .header-or-footer { padding-left: 8px; padding-right: 8px; } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 0141d6c2707..3889641d222 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -20,9 +20,10 @@ export interface IPartOptions { } export interface ILayoutContentResult { + readonly headerSize: IDimension; readonly titleSize: IDimension; readonly contentSize: IDimension; - readonly compositeBarSize: IDimension; + readonly footerSize: IDimension; } /** @@ -41,9 +42,10 @@ export abstract class Part extends Component implements ISerializableView { readonly onDidVisibilityChange = this._onDidVisibilityChange.event; private parent: HTMLElement | undefined; + private headerArea: HTMLElement | undefined; private titleArea: HTMLElement | undefined; private contentArea: HTMLElement | undefined; - private compositeBarArea: HTMLElement | undefined; + private footerArea: HTMLElement | undefined; private partLayout: PartLayout | undefined; constructor( @@ -122,35 +124,67 @@ export abstract class Part extends Component implements ISerializableView { } /** - * Creates the composite bar and sets the proper position. + * Sets the header area */ - protected setCompositeBarArea(compositeBarContainer: HTMLElement, position: 'top' | 'bottom'): void { - if (this.compositeBarArea) { - throw new Error('Composite bar already exists'); + protected setHeaderArea(headerContainer: HTMLElement): void { + if (this.headerArea) { + throw new Error('Header already exists'); } if (!this.parent || !this.titleArea) { return; } - if (position === 'top') { - prepend(this.parent, compositeBarContainer); - compositeBarContainer.classList.add('top'); - } else { - this.parent.appendChild(compositeBarContainer); - compositeBarContainer.classList.add('bottom'); - } + prepend(this.parent, headerContainer); + headerContainer.classList.add('header-or-footer'); + headerContainer.classList.add('header'); - this.compositeBarArea = compositeBarContainer; - this.partLayout?.setCompositeBarVisibility(true); + this.headerArea = headerContainer; + this.partLayout?.setHeaderVisibility(true); this.relayout(); } - protected removeCompositeBarArea(): void { - if (this.compositeBarArea) { - this.compositeBarArea.remove(); - this.compositeBarArea = undefined; - this.partLayout?.setCompositeBarVisibility(false); + /** + * Sets the footer area + */ + protected setFooterArea(footerContainer: HTMLElement): void { + if (this.footerArea) { + throw new Error('Footer already exists'); + } + + if (!this.parent || !this.titleArea) { + return; + } + + this.parent.appendChild(footerContainer); + footerContainer.classList.add('header-or-footer'); + footerContainer.classList.add('footer'); + + this.footerArea = footerContainer; + this.partLayout?.setFooterVisibility(true); + this.relayout(); + } + + /** + * removes the header area + */ + protected removeHeaderArea(): void { + if (this.headerArea) { + this.headerArea.remove(); + this.headerArea = undefined; + this.partLayout?.setHeaderVisibility(false); + this.relayout(); + } + } + + /** + * removes the footer area + */ + protected removeFooterArea(): void { + if (this.footerArea) { + this.footerArea.remove(); + this.footerArea = undefined; + this.partLayout?.setFooterVisibility(false); this.relayout(); } } @@ -197,10 +231,12 @@ export abstract class Part extends Component implements ISerializableView { class PartLayout { + private static readonly HEADER_HEIGHT = 35; private static readonly TITLE_HEIGHT = 35; - private static readonly COMPOSITE_BAR_HEIGHT = 35; + private static readonly Footer_HEIGHT = 35; - private compositeBarAreaVisible: boolean = false; + private headerVisible: boolean = false; + private footerVisible: boolean = false; constructor(private options: IPartOptions, private contentArea: HTMLElement | undefined) { } @@ -214,12 +250,20 @@ class PartLayout { titleSize = Dimension.None; } - // Footer Size: Width (Fill), Height (Variable) - let compositeBarSize: Dimension; - if (this.compositeBarAreaVisible) { - compositeBarSize = new Dimension(width, Math.min(height, PartLayout.COMPOSITE_BAR_HEIGHT)); + // Header Size: Width (Fill), Height (Variable) + let headerSize: Dimension; + if (this.headerVisible) { + headerSize = new Dimension(width, Math.min(height, PartLayout.HEADER_HEIGHT)); } else { - compositeBarSize = Dimension.None; + headerSize = Dimension.None; + } + + // Footer Size: Width (Fill), Height (Variable) + let footerSize: Dimension; + if (this.footerVisible) { + footerSize = new Dimension(width, Math.min(height, PartLayout.Footer_HEIGHT)); + } else { + footerSize = Dimension.None; } let contentWidth = width; @@ -228,18 +272,22 @@ class PartLayout { } // Content Size: Width (Fill), Height (Variable) - const contentSize = new Dimension(contentWidth, height - titleSize.height - compositeBarSize.height); + const contentSize = new Dimension(contentWidth, height - titleSize.height - headerSize.height - footerSize.height); // Content if (this.contentArea) { size(this.contentArea, contentSize.width, contentSize.height); } - return { titleSize, contentSize, compositeBarSize }; + return { headerSize, titleSize, contentSize, footerSize }; } - setCompositeBarVisibility(visible: boolean): void { - this.compositeBarAreaVisible = visible; + setFooterVisibility(visible: boolean): void { + this.footerVisible = visible; + } + + setHeaderVisibility(visible: boolean): void { + this.headerVisible = visible; } } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 146a58620b1..4adf1f7d162 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -438,7 +438,7 @@ export abstract class CompositePart extends Part { }; } - protected createCompositeBarArea(): HTMLElement { + protected createHeaderFooterCompositeBarArea(): HTMLElement { return $('.composite.composite-bar-area'); } diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index 4f7afbfab50..98f07d9cf18 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/src/vs/workbench/browser/parts/media/compositepart.css @@ -7,7 +7,7 @@ height: 100%; } -.monaco-workbench .part > .composite.composite-bar-area, +.monaco-workbench .part > .composite.header-or-footer, .monaco-workbench .part > .composite.title { display: flex; } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index a324f08d4b4..f2b2b178b81 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,18 +20,18 @@ display: none; } -.monaco-workbench .pane-composite-part > .composite-bar-area.bottom { +.monaco-workbench .pane-composite-part > .header-or-footer.bottom { border-top: 1px solid var(--vscode-sideBarSectionHeader-border); } .monaco-workbench .pane-composite-part > .title > .composite-bar-container, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container { display: flex; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; align-items: center; justify-content: center; @@ -41,13 +41,13 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { text-transform: uppercase; padding-left: 10px; padding-right: 10px; @@ -58,26 +58,26 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 24px; padding: 0 5px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { font-size: 18px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { width: 16px; height: 16px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { content: ''; width: 2px; height: 24px; @@ -92,32 +92,32 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { display: block; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { left: 1px; margin-left: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { right: 1px; margin-right: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { left: 2px; margin-left: -2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { right: 2px; margin-right: -2px; } @@ -126,10 +126,10 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { transition-delay: 0s; } @@ -138,51 +138,51 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { margin-right: 0; padding: 2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { border-radius: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { background: none !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { margin-right: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { margin-left: 8px; display: flex; align-items: center; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { margin-left: 0px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { padding: 3px 5px; border-radius: 11px; font-size: 11px; @@ -197,7 +197,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; top: 0; bottom: 0; @@ -210,7 +210,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { position: absolute; top: 11px; right: 0px; @@ -225,7 +225,7 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; -webkit-mask-size: 11px; top: 3px; @@ -234,7 +234,7 @@ /* active item indicator */ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { position: absolute; z-index: 1; bottom: 0; @@ -244,14 +244,14 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { top: -4px; left: 10px; width: calc(100% - 20px); } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { top: 1px; left: 2px; width: calc(100% - 4px); @@ -259,8 +259,8 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { content: ""; position: absolute; z-index: 1; @@ -272,24 +272,24 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { border-top-color: transparent !important; /* hides border on clicked state */ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, -.monaco-workbench .pane-composite-part > .composite-bar-area > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 876e85c3aea..fea45721bc8 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -114,8 +114,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); private compositeBarPosition: CompositeBarPosition | undefined = undefined; @@ -353,7 +353,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart { + this.headerFooterCompositeBarDispoables.add(addDisposableListener(compositeBarArea, EventType.CONTEXT_MENU, e => { this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); })); - this.separateCompositeBarDispoables.add(Gesture.addTarget(compositeBarArea)); - this.separateCompositeBarDispoables.add(addDisposableListener(compositeBarArea, GestureEventType.Contextmenu, e => { + this.headerFooterCompositeBarDispoables.add(Gesture.addTarget(compositeBarArea)); + this.headerFooterCompositeBarDispoables.add(addDisposableListener(compositeBarArea, GestureEventType.Contextmenu, e => { this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); })); return compositeBarArea; } - protected override removeCompositeBarArea(): void { - this.separateCompositeBarContainer = undefined; - this.separateCompositeBarDispoables.clear(); - super.removeCompositeBarArea(); + private removeFooterHeaderArea(header: boolean): void { + this.headerFooterCompositeBarContainer = undefined; + this.headerFooterCompositeBarDispoables.clear(); + if (header) { + this.removeHeaderArea(); + } else { + this.removeFooterArea(); + } } protected createCompositeBar(): PaneCompositeBar { From 469b7f85233665dbda0f684bde7fac72a810e62c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 Mar 2024 16:53:15 +0100 Subject: [PATCH 011/141] use footer --- src/vs/workbench/browser/parts/media/paneCompositePart.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index f2b2b178b81..4b9f6670205 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,11 +20,10 @@ display: none; } -.monaco-workbench .pane-composite-part > .header-or-footer.bottom { +.monaco-workbench .pane-composite-part > .header-or-footer.footer { border-top: 1px solid var(--vscode-sideBarSectionHeader-border); } - .monaco-workbench .pane-composite-part > .title > .composite-bar-container, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container { display: flex; From 904d3707a803375b8abd79eeaffc9f57429f3dfb Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Wed, 6 Mar 2024 15:58:17 +0000 Subject: [PATCH 012/141] Refactor for clarity --- .../output/browser/output.contribution.ts | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 595c458f2f4..01fdffa1233 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -348,47 +348,21 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { icon: Codicon.gear, order: 6 })); - const registeredLogLevels = new Map(); - this._register(toDisposable(() => dispose(registeredLogLevels.values()))); - const registerLogLevels = (logLevels: LogLevel[]) => { - let order = 0; - for (const logLevel of logLevels) { - const { original, value } = LogLevelToLocalizedString(logLevel); - registeredLogLevels.set(logLevel, registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.action.output.activeOutputLogLevel.${logLevel}`, - title: value, - toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(original), - menu: { - id: logLevelMenu, - order: order++, - group: '0_level' - } - }); - } - async run(accessor: ServicesAccessor): Promise { - const channel = that.outputService.getActiveChannel(); - if (channel) { - const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.file) { - return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); - } - } - } - })); - } + + let order = 0; + const registerLogLevel = (logLevel: LogLevel) => { + const { original, value } = LogLevelToLocalizedString(logLevel); this._register(registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.action.output.activeOutputLogLevelDefault`, - title: nls.localize('logLevelDefault.label', "Set As Default"), + id: `workbench.action.output.activeOutputLogLevel.${logLevel}`, + title: value, + toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(original), menu: { id: logLevelMenu, - order, - group: '1_default' - }, - precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.negate() + order: order++, + group: '0_level' + } }); } async run(accessor: ServicesAccessor): Promise { @@ -396,14 +370,44 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { if (channel) { const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); if (channelDescriptor?.log && channelDescriptor.file) { - const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); - return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); } } } })); }; - registerLogLevels([LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Off]); + + registerLogLevel(LogLevel.Trace); + registerLogLevel(LogLevel.Debug); + registerLogLevel(LogLevel.Info); + registerLogLevel(LogLevel.Warning); + registerLogLevel(LogLevel.Error); + registerLogLevel(LogLevel.Off); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.output.activeOutputLogLevelDefault`, + title: nls.localize('logLevelDefault.label', "Set As Default"), + menu: { + id: logLevelMenu, + order, + group: '1_default' + }, + precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.negate() + }); + } + async run(accessor: ServicesAccessor): Promise { + const channel = that.outputService.getActiveChannel(); + if (channel) { + const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); + if (channelDescriptor?.log && channelDescriptor.file) { + const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); + return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + } + } + } + })); } private registerShowLogsAction(): void { From 6536c8b0450a00cfc0e0eb361d0bf5dee70e447f Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Wed, 6 Mar 2024 15:59:13 +0000 Subject: [PATCH 013/141] Relocate listeners --- .../contrib/output/browser/outputServices.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 718be38cf42..8c05bd54823 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -125,6 +125,14 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } })); + this._register(this.loggerService.onDidChangeLogLevel(_level => { + this.setLevelContext(); + this.setLevelIsDefaultContext(); + })); + this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => { + this.setLevelIsDefaultContext(); + })); + this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); } @@ -204,34 +212,30 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.instantiationService.createInstance(OutputChannel, channelData); } + private setLevelContext(): void { + const descriptor = this.activeChannel?.outputChannelDescriptor; + const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; + this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); + } + + private async setLevelIsDefaultContext(): Promise { + const descriptor = this.activeChannel?.outputChannelDescriptor; + if (descriptor?.log) { + const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); + const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); + this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); + } else { + this.activeOutputChannelLevelIsDefaultContext.set(false); + } + } + private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; const descriptor = channel?.outputChannelDescriptor; this.activeFileOutputChannelContext.set(!!descriptor?.file); this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); - const setLevelContext = (): void => { - const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; - this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); - }; - const setLevelIsDefaultContext = async (): Promise => { - const descriptor = this.activeChannel?.outputChannelDescriptor; - if (descriptor?.log) { - const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); - const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); - this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); - } else { - this.activeOutputChannelLevelIsDefaultContext.set(false); - } - }; - this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => { - setLevelIsDefaultContext(); - })); - setLevelIsDefaultContext(); - this._register(this.loggerService.onDidChangeLogLevel(_level => { - setLevelContext(); - setLevelIsDefaultContext(); - })); - setLevelContext(); + this.setLevelIsDefaultContext(); + this.setLevelContext(); if (this.activeChannel) { this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); From ee36c8c2dfe2e279c4b58af4d47fdf6fc85bb9f5 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Wed, 6 Mar 2024 16:21:58 +0000 Subject: [PATCH 014/141] Use ILocalizedString correctly --- src/vs/platform/log/common/log.ts | 12 ++++++------ .../contrib/output/browser/output.contribution.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index b0342da2653..28fc419bbaf 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -749,12 +749,12 @@ export function LogLevelToString(logLevel: LogLevel): string { export function LogLevelToLocalizedString(logLevel: LogLevel): ILocalizedString { switch (logLevel) { - case LogLevel.Trace: return { original: 'trace', value: nls.localize('trace', "Trace") }; - case LogLevel.Debug: return { original: 'debug', value: nls.localize('debug', "Debug") }; - case LogLevel.Info: return { original: 'info', value: nls.localize('info', "Info") }; - case LogLevel.Warning: return { original: 'warn', value: nls.localize('warn', "Warning") }; - case LogLevel.Error: return { original: 'error', value: nls.localize('error', "Error") }; - case LogLevel.Off: return { original: 'off', value: nls.localize('off', "Off") }; + case LogLevel.Trace: return { original: 'Trace', value: nls.localize('trace', "Trace") }; + case LogLevel.Debug: return { original: 'Debug', value: nls.localize('debug', "Debug") }; + case LogLevel.Info: return { original: 'Info', value: nls.localize('info', "Info") }; + case LogLevel.Warning: return { original: 'Warning', value: nls.localize('warn', "Warning") }; + case LogLevel.Error: return { original: 'Error', value: nls.localize('error', "Error") }; + case LogLevel.Off: return { original: 'Off', value: nls.localize('off', "Off") }; } } diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 01fdffa1233..6d3f4d9ad31 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -30,7 +30,7 @@ import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ILoggerService, LogLevel, LogLevelToLocalizedString } from 'vs/platform/log/common/log'; +import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from 'vs/platform/log/common/log'; import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; // Register Service @@ -351,13 +351,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { let order = 0; const registerLogLevel = (logLevel: LogLevel) => { - const { original, value } = LogLevelToLocalizedString(logLevel); this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.action.output.activeOutputLogLevel.${logLevel}`, - title: value, - toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(original), + title: LogLevelToLocalizedString(logLevel).value, + toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(LogLevelToString(logLevel)), menu: { id: logLevelMenu, order: order++, From 550954c442dd76f6bf81a731d6953ee46d34fcb7 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 Mar 2024 17:57:45 +0100 Subject: [PATCH 015/141] max width --- src/vs/workbench/browser/parts/media/paneCompositePart.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 4b9f6670205..9e6e83c9645 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -29,6 +29,10 @@ display: flex; } +.monaco-workbench .pane-composite-part > .header-or-footer .composite-bar-container { + width: 100%; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; From 182fd057426da528736789d69e48bb01ac07ae8d Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 Mar 2024 18:09:53 +0100 Subject: [PATCH 016/141] Settings description --- src/vs/workbench/browser/workbench.contribution.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index fc4af190dca..f7e95d8343a 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -499,12 +499,12 @@ const registry = Registry.as(ConfigurationExtensions.Con 'type': 'string', 'enum': ['side', 'top', 'bottom', 'hidden'], 'default': 'side', - 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `side` or `top` of the Primary Side Bar or `hidden`."), + 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `side` or `top` / `bottom` of the Primary and Secondary Side Bar or `hidden`."), 'enumDescriptions': [ - localize('workbench.activityBar.location.side', "Show the Activity Bar to the side of the Primary Side Bar."), - localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary Side Bar."), - localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary Side Bar."), - localize('workbench.activityBar.location.hide', "Hide the Activity Bar.") + localize('workbench.activityBar.location.side', "Show the Activity Bar of the Primary Side Bar on the side."), + localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bar."), + localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bar."), + localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bar.") ], }, 'workbench.activityBar.iconClickBehavior': { From 828d3c030ec37dcfbe6668be80cb7243735e25b1 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 6 Mar 2024 10:48:18 -0800 Subject: [PATCH 017/141] don't use copy action --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 0db49de0bf8..266c30bfe98 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -95,11 +95,7 @@ export function registerChatCodeBlockActions() { } run(accessor: ServicesAccessor, ...args: any[]) { - const accessibleViewService = accessor.get(IAccessibleViewService); - let context = args[0]; - if (!context) { - context = accessibleViewService.getCodeBlockContext(); - } + const context = args[0]; if (!isCodeBlockActionContext(context) || isResponseFiltered(context)) { return; } From 7bbdb593f4b3f8d0efeccf5c195531b8510a70c0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 6 Mar 2024 10:50:37 -0800 Subject: [PATCH 018/141] only calculate for chat provider --- .../workbench/contrib/accessibility/browser/accessibleView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 294cc709daf..e07c6a84c1b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -362,7 +362,7 @@ export class AccessibleView extends Disposable { } calculateCodeBlocks(markdown: string): void { - if (!this._currentProvider) { + if (!this._currentProvider || this._currentProvider.options.id !== AccessibleViewProviderId.Chat) { return; } if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { From 14faed3b68c7a2739bf7172397d40101634fb3d0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 6 Mar 2024 11:13:14 -0800 Subject: [PATCH 019/141] add to accessibiliity help dialog --- .../accessibility/browser/accessibleView.ts | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index e07c6a84c1b..b8411ddadfe 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -492,11 +492,8 @@ export class AccessibleView extends Disposable { } private _render(provider: IAccessibleContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { - if (!showAccessibleViewHelp) { - // don't overwrite the current provider - this._currentProvider = provider; - this._accessibleViewCurrentProviderId.set(provider.id); - } + this._currentProvider = provider; + this._accessibleViewCurrentProviderId.set(provider.id); const value = this._configurationService.getValue(provider.verbositySettingKey); const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; let disableHelpHint = ''; @@ -684,6 +681,7 @@ export class AccessibleView extends Disposable { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))."); + const chatHints = this._getChatHints(); let hint = localize('intro', "In the accessible view, you can:\n"); if (navigationHint) { @@ -695,6 +693,37 @@ export class AccessibleView extends Disposable { if (toolbarHint) { hint += ' - ' + toolbarHint + '\n'; } + if (chatHints) { + hint += chatHints; + } + return hint; + } + + private _getChatHints(): string | undefined { + if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { + return; + } + let hint = ''; + const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel(); + const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel(); + const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel(); + + if (insertAtCursorKb) { + hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb); + } else { + hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n"); + } + if (insertIntoNewFileKb) { + hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb); + } else { + hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert at Cursor command.\n"); + } + if (runInTerminalKb) { + hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb); + } else { + hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n"); + } + return hint; } From 314cf4ba4c15014c93684225783c9375708e3cec Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 7 Mar 2024 12:12:00 +0100 Subject: [PATCH 020/141] integrity - remove banner (#206874) (#207050) --- .../electron-sandbox/integrityService.ts | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts index 1834c7f3f53..3789a7c1e97 100644 --- a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts +++ b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts @@ -16,8 +16,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileAccess, AppResourcePath } from 'vs/base/common/network'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; -import { Codicon } from 'vs/base/common/codicons'; interface IStorageData { readonly dontShowPrompt: boolean; @@ -75,8 +73,7 @@ export class IntegrityService implements IIntegrityService { @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, @IChecksumService private readonly checksumService: IChecksumService, - @ILogService private readonly logService: ILogService, - @IBannerService private readonly bannerService: IBannerService + @ILogService private readonly logService: ILogService ) { this._compute(); } @@ -89,9 +86,9 @@ export class IntegrityService implements IIntegrityService { this.logService.warn(` -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!!! Installation has been modified on disk and is UNSUPPORTED. Please reinstall !!! -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +---------------------------------------------- +*** Installation has been modified on disk *** +---------------------------------------------- `); @@ -100,7 +97,6 @@ export class IntegrityService implements IIntegrityService { return; // Do not prompt } - this._showBanner(); this._showNotification(); } @@ -146,22 +142,6 @@ export class IntegrityService implements IIntegrityService { }; } - private _showBanner(): void { - const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; - - this.bannerService.show({ - id: 'installation.corrupt', - message: localize('integrity.banner', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort), - icon: Codicon.warning, - actions: checksumFailMoreInfoUrl ? [ - { - label: localize('integrity.moreInformation', "More Information"), - href: checksumFailMoreInfoUrl - } - ] : undefined - }); - } - private _showNotification(): void { const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); From e563009edb2ba8cc4d9019cf17d2b232c4c36a8e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:14:18 +0100 Subject: [PATCH 021/141] Remove padding due to using compact (#207053) remove padding due to compact --- .../contrib/languageStatus/browser/media/languageStatus.css | 1 - src/vs/workbench/electron-sandbox/media/window.css | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index f7fb23f59c6..4354ad022df 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -55,7 +55,6 @@ .monaco-workbench .hover-language-status { display: flex; - padding: 4px 8px; } .monaco-workbench .hover-language-status:not(:last-child) { diff --git a/src/vs/workbench/electron-sandbox/media/window.css b/src/vs/workbench/electron-sandbox/media/window.css index 8bf36659380..b51743e9b57 100644 --- a/src/vs/workbench/electron-sandbox/media/window.css +++ b/src/vs/workbench/electron-sandbox/media/window.css @@ -5,10 +5,6 @@ .monaco-workbench .zoom-status { display: flex; - padding-top: 2px; - padding-bottom: 2px; - padding-left: 5px; - padding-right: 5px; } .monaco-workbench .zoom-status .monaco-action-bar .action-label.codicon::before { From 1517ee858a4308abe3735341e84190709fb6647d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:14:31 +0100 Subject: [PATCH 022/141] Use default hideOnHover behaviour in WorkbenchHoverDelegate (#207051) hideOnHover default behaviour WorkbenchHoverDelegate --- src/vs/platform/hover/browser/hover.ts | 1 - src/vs/workbench/browser/parts/views/treeView.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index 0a60a0b4556..9213e1ea02f 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -286,7 +286,6 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate ...options, ...overrideOptions, persistence: { - hideOnHover: true, hideOnKeyDown: true, ...overrideOptions.persistence }, diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 2af2a8be2e5..661638fa561 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -1106,7 +1106,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer this.rerender())); this._register(this.themeService.onDidColorThemeChange(() => this.rerender())); this._register(checkboxStateHandler.onDidChangeCheckboxState(items => { From c8cdd19acf1befb7e49bc9d5104b74715b18986e Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 7 Mar 2024 12:24:10 +0100 Subject: [PATCH 023/141] Fix exception when hover is not defined before setting timestamp (#207055) --- src/vs/workbench/contrib/comments/browser/timestamp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 16b3969d2c3..dbfad43dfd0 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -24,8 +24,8 @@ export class TimestampWidget extends Disposable { this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; - this.setTimestamp(timeStamp); this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._date, '')); + this.setTimestamp(timeStamp); } private get useRelativeTimeSetting(): boolean { From 54eb843be2182ed2452a7975f5b31147f2442dea Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 7 Mar 2024 12:49:39 +0100 Subject: [PATCH 024/141] restart extension host for updating extensions instead of reload (#206997) * #125417 restart extension host for updating extensions instead of reload * :lipstick: * team feedback * fix enum const --- .../extensions/browser/extensionEditor.ts | 4 +- .../extensions/browser/extensionsActions.ts | 20 +-- .../extensions/browser/extensionsList.ts | 4 +- .../browser/extensionsWorkbenchService.ts | 51 +++++-- .../contrib/extensions/common/extensions.ts | 3 +- .../extensionsActions.test.ts | 134 +++++++++--------- .../common/abstractExtensionService.ts | 6 +- .../services/extensions/common/extensions.ts | 4 +- 8 files changed, 132 insertions(+), 94 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 34c163c0b47..b9de3cbbb0c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -61,7 +61,7 @@ import { InstallDropdownAction, InstallingLabelAction, LocalInstallAction, MigrateDeprecatedExtensionAction, - ReloadAction, + ExtensionRuntimeStateAction, RemoteInstallAction, SetColorThemeAction, SetFileIconThemeAction, @@ -315,7 +315,7 @@ export class ExtensionEditor extends EditorPane { const installAction = this.instantiationService.createInstance(InstallDropdownAction); const actions = [ - this.instantiationService.createInstance(ReloadAction), + this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2f2833049ba..43e8a7c086d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1554,20 +1554,20 @@ export class DisableDropDownAction extends ActionWithDropDownAction { } -export class ReloadAction extends ExtensionAction { +export class ExtensionRuntimeStateAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; - private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${ExtensionRuntimeStateAction.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; constructor( - @IHostService private readonly hostService: IHostService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, @IProductService private readonly productService: IProductService, ) { - super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); + super('extensions.runtimeState', '', ExtensionRuntimeStateAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this.update(); } @@ -1575,7 +1575,7 @@ export class ReloadAction extends ExtensionAction { update(): void { this.enabled = false; this.tooltip = ''; - this.class = ReloadAction.DisabledClass; + this.class = ExtensionRuntimeStateAction.DisabledClass; if (!this.extension) { return; @@ -1596,18 +1596,18 @@ export class ReloadAction extends ExtensionAction { } this.enabled = true; - this.class = ReloadAction.EnabledClass; + this.class = ExtensionRuntimeStateAction.EnabledClass; this.tooltip = runtimeState.reason; - this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload window', 'Reload Window') - : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart {0}', this.productService.nameShort) + this.label = runtimeState.action === ExtensionRuntimeActionType.RestartExtensions ? localize('restart extensions', 'Restart Extensions') + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart to Update') : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } override async run(): Promise { const runtimeState = this.extension?.runtimeState; - if (runtimeState?.action === ExtensionRuntimeActionType.Reload) { - return this.hostService.reload(); + if (runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions) { + return this.extensionsWorkbenchService.updateRunningExtensions(); } else if (runtimeState?.action === ExtensionRuntimeActionType.DownloadUpdate) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index d5058c51b43..2595d6010cc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,7 +13,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -117,7 +117,7 @@ export class Renderer implements IPagedRenderer { const actions = [ this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), - this.instantiationService.createInstance(ReloadAction), + this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), this.instantiationService.createInstance(InstallDropdownAction), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 903dcac8f25..ec74cf72646 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1111,6 +1111,39 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } + async updateRunningExtensions(): Promise { + const toAdd: ILocalExtension[] = []; + const toRemove: string[] = []; + for (const extension of this.local) { + const runtimeState = extension.runtimeState; + if (!runtimeState || runtimeState.action !== ExtensionRuntimeActionType.RestartExtensions) { + continue; + } + if (extension.state === ExtensionState.Uninstalled) { + toRemove.push(extension.identifier.id); + continue; + } + if (!extension.local) { + continue; + } + const isEnabled = this.extensionEnablementService.isEnabled(extension.local); + if (isEnabled) { + const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); + if (runningExtension) { + toRemove.push(runningExtension.identifier.value); + } + toAdd.push(extension.local); + } else { + toRemove.push(extension.identifier.id); + } + } + if (toAdd.length || toRemove.length) { + if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"))) { + await this.extensionService.startExtensionHosts({ toAdd, toRemove }); + } + } + } + private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); @@ -1119,7 +1152,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUninstallTooltip', "Please reload {0} to complete the uninstallation of this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postUninstallTooltip', "Please restart extensions to complete the uninstallation of this extension.") }; } return undefined; } @@ -1157,7 +1190,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } return undefined; } - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUpdateTooltip', "Please reload {0} to enable the updated extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postUpdateTooltip', "Please restart extensions to enable the updated extension.") }; } if (this.extensionsServers.length > 1) { @@ -1165,12 +1198,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable locally', "Please reload {0} to enable this extension locally.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('enable locally', "Please restart extensions to enable this extension locally.") }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable remote', "Please reload {0} to enable this extension in {1}.", this.productService.nameLong, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('enable remote', "Please restart extensions to enable this extension in {1}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1180,20 +1213,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; } } } return undefined; } else { if (isSameExtensionRunning) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postDisableTooltip', "Please reload {0} to disable this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postDisableTooltip', "Please restart extensions to disable this extension.") }; } } return undefined; @@ -1202,7 +1235,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1210,7 +1243,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; + return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; } } } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index b523d97d6b2..8be5639f119 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -40,7 +40,7 @@ export const enum ExtensionState { } export const enum ExtensionRuntimeActionType { - Reload = 'reload', + RestartExtensions = 'restartExtensions', DownloadUpdate = 'downloadUpdate', ApplyUpdate = 'applyUpdate', QuitAndInstall = 'quitAndInstall', @@ -139,6 +139,7 @@ export interface IExtensionsWorkbenchService { checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; updateAll(): Promise; + updateRunningExtensions(): Promise; // Sync APIs isExtensionIgnoredToSync(extension: IExtension): boolean; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 3ed7d4a1b94..f8c7f267f13 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -950,21 +950,21 @@ suite('ExtensionsActions', () => { }); -suite('ReloadAction', () => { +suite('ExtensionRuntimeStateAction', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => setupTest(disposables)); - test('Test ReloadAction when there is no extension', () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when there is no extension', () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension state is installing', async () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when extension state is installing', async () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const gallery = aGalleryExtension('a'); @@ -976,8 +976,8 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension state is uninstalling', async () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when extension state is uninstalling', async () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -988,7 +988,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is newly installed', async () => { + test('Test Runtime State when extension is newly installed', async () => { const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], @@ -996,7 +996,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1010,10 +1010,10 @@ suite('ReloadAction', () => { didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); + assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); }); - test('Test ReloadAction when extension is newly installed and reload is not required', async () => { + test('Test Runtime State when extension is newly installed and ext host restart is not required', async () => { const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], @@ -1021,7 +1021,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1035,7 +1035,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is installed and uninstalled', async () => { + test('Test Runtime State when extension is installed and uninstalled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1043,7 +1043,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1059,7 +1059,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled', async () => { + test('Test Runtime State when extension is uninstalled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1068,7 +1068,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1078,10 +1078,10 @@ suite('ReloadAction', () => { uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to complete the uninstallation of this extension.`); + assert.strictEqual(testObject.tooltip, `Please restart extensions to complete the uninstallation of this extension.`); }); - test('Test ReloadAction when extension is uninstalled and can be removed', async () => { + test('Test Runtime State when extension is uninstalled and can be removed', async () => { const local = aLocalExtension('a'); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(local)], @@ -1090,7 +1090,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); @@ -1101,7 +1101,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled and installed', async () => { + test('Test Runtime State when extension is uninstalled and installed', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1109,7 +1109,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1127,7 +1127,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is updated while running', async () => { + test('Test Runtime State when extension is updated while running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], onDidChangeExtensions: Event.None, @@ -1136,7 +1136,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1' }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1146,7 +1146,7 @@ suite('ReloadAction', () => { return new Promise(c => { disposables.add(testObject.onDidChange(() => { - if (testObject.enabled && testObject.tooltip === `Please reload ${instantiationService.get(IProductService).nameLong} to enable the updated extension.`) { + if (testObject.enabled && testObject.tooltip === `Please restart extensions to enable the updated extension.`) { c(); } })); @@ -1156,7 +1156,7 @@ suite('ReloadAction', () => { }); }); - test('Test ReloadAction when extension is updated when not running', async () => { + test('Test Runtime State when extension is updated when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1166,7 +1166,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1180,7 +1180,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is disabled when running', async () => { + test('Test Runtime State when extension is disabled when running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a'))], onDidChangeExtensions: Event.None, @@ -1189,7 +1189,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1200,10 +1200,10 @@ suite('ReloadAction', () => { await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to disable this extension.`, testObject.tooltip); + assert.strictEqual(`Please restart extensions to disable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when extension enablement is toggled when running', async () => { + test('Test Runtime State when extension enablement is toggled when running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1212,7 +1212,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1224,7 +1224,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is enabled when not running', async () => { + test('Test Runtime State when extension is enabled when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1234,7 +1234,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1243,10 +1243,10 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); + assert.strictEqual(`Please restart extensions to enable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when extension enablement is toggled when not running', async () => { + test('Test Runtime State when extension enablement is toggled when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1256,7 +1256,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1267,7 +1267,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is updated when not running and enabled', async () => { + test('Test Runtime State when extension is updated when not running and enabled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a'))], onDidChangeExtensions: Event.None, @@ -1277,7 +1277,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1290,10 +1290,10 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); + assert.strictEqual(`Please restart extensions to enable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when a localization extension is newly installed', async () => { + test('Test Runtime State when a localization extension is newly installed', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1301,7 +1301,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1315,7 +1315,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when a localization extension is updated while running', async () => { + test('Test Runtime State when a localization extension is updated while running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], onDidChangeExtensions: Event.None, @@ -1323,7 +1323,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1', contributes: { localizations: [{ languageId: 'de', translations: [] }] } }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1337,7 +1337,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is not installed but extension from different server is installed and running', async () => { + test('Test Runtime State when extension is not installed but extension from different server is installed and running', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); @@ -1355,7 +1355,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1366,7 +1366,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled but extension from different server is installed and running', async () => { + test('Test Runtime State when extension is uninstalled but extension from different server is installed and running', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); @@ -1389,7 +1389,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1405,7 +1405,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when workspace extension is disabled on local server and installed in remote server', async () => { + test('Test Runtime State when workspace extension is disabled on local server and installed in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const remoteExtensionManagementService = createExtensionManagementService([]); @@ -1425,7 +1425,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1441,10 +1441,10 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); + assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); }); - test('Test ReloadAction when ui extension is disabled on remote server and installed in local server', async () => { + test('Test Runtime State when ui extension is disabled on remote server and installed in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtensionManagementService = createExtensionManagementService([]); @@ -1464,7 +1464,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1480,10 +1480,10 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); + assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); }); - test('Test ReloadAction for remote ui extension is disabled when it is installed and enabled in local server', async () => { + test('Test Runtime State for remote ui extension is disabled when it is installed and enabled in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a') }); @@ -1504,7 +1504,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1515,7 +1515,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction for remote workspace+ui extension is enabled when it is installed and enabled in local server', async () => { + test('Test Runtime State for remote workspace+ui extension is enabled when it is installed and enabled in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a') }); @@ -1536,7 +1536,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1547,7 +1547,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for local ui+workspace extension is enabled when it is installed and enabled in remote server', async () => { + test('Test Runtime State for local ui+workspace extension is enabled when it is installed and enabled in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file('pub.a') }); @@ -1568,7 +1568,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1579,7 +1579,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for local workspace+ui extension is enabled when it is installed in both servers but running in local server', async () => { + test('Test Runtime State for local workspace+ui extension is enabled when it is installed in both servers but running in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a') }); @@ -1600,7 +1600,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1611,7 +1611,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for remote ui+workspace extension is enabled when it is installed on both servers but running in remote server', async () => { + test('Test Runtime State for remote ui+workspace extension is enabled when it is installed on both servers but running in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file('pub.a') }); @@ -1632,7 +1632,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1643,7 +1643,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction when ui+workspace+web extension is installed in web and remote and running in remote', async () => { + test('Test Runtime State when ui+workspace+web extension is installed in web and remote and running in remote', async () => { // multi server setup const gallery = aGalleryExtension('a'); const webExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'], 'browser': 'browser.js' }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeUserData }) }); @@ -1660,7 +1660,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1671,7 +1671,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when workspace+ui+web extension is installed in web and local and running in local', async () => { + test('Test Runtime State when workspace+ui+web extension is installed in web and local and running in local', async () => { // multi server setup const gallery = aGalleryExtension('a'); const webExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'], 'browser': 'browser.js' }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeUserData }) }); @@ -1688,7 +1688,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 2d93e41d0f0..0336227d2b7 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -864,9 +864,13 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - public async startExtensionHosts(): Promise { + public async startExtensionHosts(updates?: { toAdd: IExtension[]; toRemove: string[] }): Promise { this._doStopExtensionHosts(); + if (updates) { + await this._handleDeltaExtensions(new DeltaExtensionsQueueItem(updates.toAdd, updates.toRemove)); + } + const lock = await this._registry.acquireLock('startExtensionHosts'); try { this._startExtensionHostsIfNecessary(false, Array.from(this._allRequestedActivateEvents.keys())); diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 5c68e7480b7..1b9f047f5c0 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -519,9 +519,9 @@ export interface IExtensionService { stopExtensionHosts(reason: string): Promise; /** - * Starts the extension hosts. + * Starts the extension hosts. If updates are provided, the extension hosts are started with the given updates. */ - startExtensionHosts(): Promise; + startExtensionHosts(updates?: { readonly toAdd: readonly IExtension[]; readonly toRemove: readonly string[] }): Promise; /** * Modify the environment of the remote extension host From 351654be4d3201e07088593adbbfa1176ec4a3ba Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 7 Mar 2024 12:52:03 +0100 Subject: [PATCH 025/141] Close Action in header and hide title label when empty --- src/vs/platform/actions/common/actions.ts | 1 + .../parts/auxiliarybar/auxiliaryBarPart.ts | 42 ++++++++++++++- .../workbench/browser/parts/compositePart.ts | 10 ++-- .../browser/parts/media/paneCompositePart.css | 2 +- .../browser/parts/paneCompositePart.ts | 54 +++++++++++++------ .../browser/parts/panel/panelActions.ts | 9 +++- 6 files changed, 96 insertions(+), 22 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 98cd2259f98..3a99d5d8a9a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -194,6 +194,7 @@ export class MenuId { static readonly SidebarTitle = new MenuId('SidebarTitle'); static readonly PanelTitle = new MenuId('PanelTitle'); static readonly AuxiliaryBarTitle = new MenuId('AuxiliaryBarTitle'); + static readonly AuxiliaryBarHeader = new MenuId('AuxiliaryBarHeader'); static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext'); static readonly TerminalEditorInstanceContext = new MenuId('TerminalEditorInstanceContext'); static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext'); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index e01dc69a69d..675f19df25e 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -26,11 +26,15 @@ import { LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; import { ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/browser/parts/paneCompositePart'; -import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { $ } from 'vs/base/browser/dom'; +import { HiddenItemStrategy, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { @@ -210,6 +214,42 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { } } + protected override createHeaderArea() { + const headerArea = super.createHeaderArea(); + const globalHeaderContainer = $('.auxiliary-bar-global-header'); + + // Add auxillary header action + const menu = this.headerFooterCompositeBarDispoables.add(this.instantiationService.createInstance(CompositeMenuActions, MenuId.AuxiliaryBarHeader, undefined, undefined)); + + const toolBar = this.headerFooterCompositeBarDispoables.add(this.instantiationService.createInstance(WorkbenchToolBar, globalHeaderContainer, { + actionViewItemProvider: (action, options) => this.headerActionViewItemProvider(action, options), + orientation: ActionsOrientation.HORIZONTAL, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + })); + + toolBar.setActions(prepareActions(menu.getPrimaryActions())); + this.headerFooterCompositeBarDispoables.add(menu.onDidChange(() => toolBar.setActions(prepareActions(menu.getPrimaryActions())))); + + headerArea.appendChild(globalHeaderContainer); + return headerArea; + } + + protected override getToolbarWidth(): number { + if (this.getCompositeBarPosition() === CompositeBarPosition.TOP) { + return 22; + } + return super.getToolbarWidth(); + } + + private headerActionViewItemProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { + if (action.id === ToggleAuxiliaryBarAction.ID) { + return this.instantiationService.createInstance(ActionViewItem, undefined, action, options); + } + + return undefined; + } + override toJSON(): object { return { type: Parts.AUXILIARYBAR_PART diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 4adf1f7d162..d1617f55cb5 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -70,7 +70,7 @@ export abstract class CompositePart extends Part { private activeComposite: Composite | undefined; private lastActiveCompositeId: string; private readonly instantiatedCompositeItems = new Map(); - private titleLabel: ICompositeTitleLabel | undefined; + protected titleLabel: ICompositeTitleLabel | undefined; private progressBar: ProgressBar | undefined; private contentAreaSize: Dimension | undefined; private readonly actionsListener = this._register(new MutableDisposable()); @@ -438,8 +438,12 @@ export abstract class CompositePart extends Part { }; } - protected createHeaderFooterCompositeBarArea(): HTMLElement { - return $('.composite.composite-bar-area'); + protected createHeaderArea(): HTMLElement { + return $('.composite'); + } + + protected createFooterArea(): HTMLElement { + return $('.composite'); } override updateStyles(): void { diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 9e6e83c9645..2155798ed82 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -30,7 +30,7 @@ } .monaco-workbench .pane-composite-part > .header-or-footer .composite-bar-container { - width: 100%; + flex: 1; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index fea45721bc8..d914587e771 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -115,7 +115,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); private compositeBarPosition: CompositeBarPosition | undefined = undefined; @@ -372,7 +372,12 @@ export abstract class AbstractPaneCompositePart extends CompositePart { - this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); + protected override createFooterArea(): HTMLElement { + const footerArea = super.createFooterArea(); + return this.createHeaderFooterCompositeBarArea(footerArea); + } + + protected createHeaderFooterCompositeBarArea(area: HTMLElement): HTMLElement { + if (this.headerFooterCompositeBarContainer) { + // A pane composite part has either a header or a footer, but not both + throw new Error('Header or Footer composite bar already exists'); + } + this.headerFooterCompositeBarContainer = area; + + this.headerFooterCompositeBarDispoables.add(addDisposableListener(area, EventType.CONTEXT_MENU, e => { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(area), e)); })); - this.headerFooterCompositeBarDispoables.add(Gesture.addTarget(compositeBarArea)); - this.headerFooterCompositeBarDispoables.add(addDisposableListener(compositeBarArea, GestureEventType.Contextmenu, e => { - this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(compositeBarArea), e)); + this.headerFooterCompositeBarDispoables.add(Gesture.addTarget(area)); + this.headerFooterCompositeBarDispoables.add(addDisposableListener(area, GestureEventType.Contextmenu, e => { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(area), e)); })); - return compositeBarArea; + return area; } private removeFooterHeaderArea(header: boolean): void { @@ -528,15 +546,17 @@ export abstract class AbstractPaneCompositePart extends CompositePart Date: Thu, 7 Mar 2024 13:40:24 +0100 Subject: [PATCH 026/141] Add context menu to comments in comments panel to un/resolve conversation (#207056) Part of #206898 --- src/vs/editor/common/languages.ts | 2 +- src/vs/monaco.d.ts | 2 +- src/vs/platform/actions/common/actions.ts | 1 + .../api/browser/mainThreadComments.ts | 12 +- .../comments/browser/commentService.ts | 138 ++++++++-------- .../browser/commentThreadZoneWidget.ts | 14 +- .../comments/browser/commentsController.ts | 88 +++++----- .../contrib/comments/browser/commentsModel.ts | 30 ++-- .../comments/browser/commentsTreeViewer.ts | 150 ++++++++++++++++-- .../contrib/comments/browser/commentsView.ts | 32 ---- .../contrib/comments/browser/media/panel.css | 36 +++++ .../contrib/comments/common/commentModel.ts | 39 +++-- .../test/browser/commentsView.test.ts | 1 + .../browser/view/cellParts/cellComments.ts | 2 +- .../actions/common/menusExtensionPoint.ts | 6 + .../common/extensionsApiProposals.ts | 1 + ...oposed.contribCommentsViewThreadMenus.d.ts | 6 + 17 files changed, 353 insertions(+), 207 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 4d157bf5788..5af870d82ff 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1883,7 +1883,7 @@ export interface PendingCommentThread { body: string; range: IRange | undefined; uri: URI; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 3391b58aa2e..7553f3a2623 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7838,7 +7838,7 @@ declare namespace monaco.languages { body: string; range: IRange | undefined; uri: Uri; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 98cd2259f98..b14c7d0b536 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -162,6 +162,7 @@ export class MenuId { static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext'); static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); + static readonly CommentsViewThreadActions = new MenuId('CommentsViewThreadActions'); static readonly InteractiveToolbar = new MenuId('InteractiveToolbar'); static readonly InteractiveCellTitle = new MenuId('InteractiveCellTitle'); static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 4e369eb19da..8896e0b6e1c 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -248,6 +248,10 @@ export class MainThreadCommentController implements ICommentController { return this._features; } + get owner() { + return this._id; + } + constructor( private readonly _proxy: ExtHostCommentsShape, private readonly _commentService: ICommentService, @@ -385,7 +389,7 @@ export class MainThreadCommentController implements ICommentController { async getDocumentComments(resource: URI, token: CancellationToken) { if (resource.scheme === Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [], commentingRanges: { @@ -407,7 +411,7 @@ export class MainThreadCommentController implements ICommentController { const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret, commentingRanges: { @@ -421,7 +425,7 @@ export class MainThreadCommentController implements ICommentController { async getNotebookComments(resource: URI, token: CancellationToken) { if (resource.scheme !== Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [] }; @@ -436,7 +440,7 @@ export class MainThreadCommentController implements ICommentController { } return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret }; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index da253044635..accc000bdce 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -31,14 +31,14 @@ interface IResourceCommentThreadEvent { } export interface ICommentInfo extends CommentInfo { - owner: string; + uniqueOwner: string; label?: string; } export interface INotebookCommentInfo { extensionId?: string; threads: CommentThread[]; - owner: string; + uniqueOwner: string; label?: string; } @@ -49,7 +49,7 @@ export interface IWorkspaceCommentThreadsEvent { } export interface INotebookCommentThreadChangedEvent extends CommentThreadChangedEvent { - owner: string; + uniqueOwner: string; } export interface ICommentController { @@ -62,6 +62,7 @@ export interface ICommentController { }; options?: CommentOptions; contextValue?: string; + owner: string; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; @@ -83,7 +84,7 @@ export interface ICommentService { readonly onDidUpdateNotebookCommentThreads: Event; readonly onDidChangeActiveEditingCommentThread: Event; readonly onDidChangeCurrentCommentThread: Event; - readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; @@ -91,28 +92,28 @@ export interface ICommentService { readonly isCommentingEnabled: boolean; readonly commentsModel: ICommentsModel; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; - removeWorkspaceComments(owner: string): void; - registerCommentController(owner: string, commentControl: ICommentController): void; - unregisterCommentController(owner?: string): void; - getCommentController(owner: string): ICommentController | undefined; - createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise; - updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise; - getCommentMenus(owner: string): CommentMenus; + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void; + removeWorkspaceComments(uniqueOwner: string): void; + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; + unregisterCommentController(uniqueOwner?: string): void; + getCommentController(uniqueOwner: string): ICommentController | undefined; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; + getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void; disposeCommentThread(ownerId: string, threadId: string): void; getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]>; getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]>; updateCommentingRanges(ownerId: string, resourceHints?: CommentingRangeResourceHint): void; - hasReactionHandler(owner: string): boolean; - toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; + hasReactionHandler(uniqueOwner: string): boolean; + toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; setActiveEditingCommentThread(commentThread: CommentThread | null): void; setCurrentCommentThread(commentThread: CommentThread | undefined): void; - setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; + setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; enableCommenting(enable: boolean): void; registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; - removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; + removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined; resourceHasCommentingRanges(resource: URI): boolean; } @@ -139,8 +140,8 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateNotebookCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateNotebookCommentThreads: Event = this._onDidUpdateNotebookCommentThreads.event; - private readonly _onDidUpdateCommentingRanges: Emitter<{ owner: string }> = this._register(new Emitter<{ owner: string }>()); - readonly onDidUpdateCommentingRanges: Event<{ owner: string }> = this._onDidUpdateCommentingRanges.event; + private readonly _onDidUpdateCommentingRanges: Emitter<{ uniqueOwner: string }> = this._register(new Emitter<{ uniqueOwner: string }>()); + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }> = this._onDidUpdateCommentingRanges.event; private readonly _onDidChangeActiveEditingCommentThread = this._register(new Emitter()); readonly onDidChangeActiveEditingCommentThread = this._onDidChangeActiveEditingCommentThread.event; @@ -165,7 +166,7 @@ export class CommentService extends Disposable implements ICommentService { private _isCommentingEnabled: boolean = true; private _workspaceHasCommenting: IContextKey; - private _continueOnComments = new Map(); // owner -> PendingCommentThread[] + private _continueOnComments = new Map(); // uniqueOwner -> PendingCommentThread[] private _continueOnCommentProviders = new Set(); private readonly _commentsModel: CommentsModel = this._register(new CommentsModel()); @@ -200,15 +201,16 @@ export class CommentService extends Disposable implements ICommentService { } this.logService.debug(`Comments: URIs of continue on comments from storage ${commentsToRestore.map(thread => thread.uri.toString()).join(', ')}.`); const changedOwners = this._addContinueOnComments(commentsToRestore, this._continueOnComments); - for (const owner of changedOwners) { - const control = this._commentControls.get(owner); + for (const uniqueOwner of changedOwners) { + const control = this._commentControls.get(uniqueOwner); if (!control) { continue; } const evt: ICommentThreadChangedEvent = { - owner, + uniqueOwner: uniqueOwner, + owner: control.owner, ownerLabel: control.label, - pending: this._continueOnComments.get(owner) || [], + pending: this._continueOnComments.get(uniqueOwner) || [], added: [], removed: [], changed: [] @@ -294,8 +296,8 @@ export class CommentService extends Disposable implements ICommentService { } private _lastActiveCommentController: ICommentController | undefined; - async setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { - const commentController = this._commentControls.get(owner); + async setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -312,8 +314,8 @@ export class CommentService extends Disposable implements ICommentService { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } - private setModelThreads(ownerId: string, ownerLabel: string, commentThreads: CommentThread[]) { - this._commentsModel.setCommentThreads(ownerId, ownerLabel, commentThreads); + private setModelThreads(ownerId: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]) { + this._commentsModel.setCommentThreads(ownerId, owner, ownerLabel, commentThreads); this._onDidSetAllCommentThreads.fire({ ownerId, ownerLabel, commentThreads }); } @@ -322,45 +324,45 @@ export class CommentService extends Disposable implements ICommentService { this._onDidUpdateCommentThreads.fire(event); } - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void { + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void { if (commentsByResource.length) { this._workspaceHasCommenting.set(true); } - const control = this._commentControls.get(owner); + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, commentsByResource); + this.setModelThreads(uniqueOwner, control.owner, control.label, commentsByResource); } } - removeWorkspaceComments(owner: string): void { - const control = this._commentControls.get(owner); + removeWorkspaceComments(uniqueOwner: string): void { + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, []); + this.setModelThreads(uniqueOwner, control.owner, control.label, []); } } - registerCommentController(owner: string, commentControl: ICommentController): void { - this._commentControls.set(owner, commentControl); + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void { + this._commentControls.set(uniqueOwner, commentControl); this._onDidSetDataProvider.fire(); } - unregisterCommentController(owner?: string): void { - if (owner) { - this._commentControls.delete(owner); + unregisterCommentController(uniqueOwner?: string): void { + if (uniqueOwner) { + this._commentControls.delete(uniqueOwner); } else { this._commentControls.clear(); } - this._commentsModel.deleteCommentsByOwner(owner); - this._onDidDeleteDataProvider.fire(owner); + this._commentsModel.deleteCommentsByOwner(uniqueOwner); + this._onDidDeleteDataProvider.fire(uniqueOwner); } - getCommentController(owner: string): ICommentController | undefined { - return this._commentControls.get(owner); + getCommentController(uniqueOwner: string): ICommentController | undefined { + return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise { - const commentController = this._commentControls.get(owner); + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -369,8 +371,8 @@ export class CommentService extends Disposable implements ICommentService { return commentController.createCommentThreadTemplate(resource, range); } - async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range) { - const commentController = this._commentControls.get(owner); + async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -379,31 +381,31 @@ export class CommentService extends Disposable implements ICommentService { await commentController.updateCommentThreadTemplate(threadHandle, range); } - disposeCommentThread(owner: string, threadId: string) { - const controller = this.getCommentController(owner); + disposeCommentThread(uniqueOwner: string, threadId: string) { + const controller = this.getCommentController(uniqueOwner); controller?.deleteCommentThreadMain(threadId); } - getCommentMenus(owner: string): CommentMenus { - if (this._commentMenus.get(owner)) { - return this._commentMenus.get(owner)!; + getCommentMenus(uniqueOwner: string): CommentMenus { + if (this._commentMenus.get(uniqueOwner)) { + return this._commentMenus.get(uniqueOwner)!; } const menu = this.instantiationService.createInstance(CommentMenus); - this._commentMenus.set(owner, menu); + this._commentMenus.set(uniqueOwner, menu); return menu; } updateComments(ownerId: string, event: CommentThreadChangedEvent): void { const control = this._commentControls.get(ownerId); if (control) { - const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId, ownerLabel: control.label }); + const evt: ICommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId, ownerLabel: control.label, owner: control.owner }); this.updateModelThreads(evt); } } updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void { - const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId }); + const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId }); this._onDidUpdateNotebookCommentThreads.fire(evt); } @@ -414,11 +416,11 @@ export class CommentService extends Disposable implements ICommentService { } } this._workspaceHasCommenting.set(true); - this._onDidUpdateCommentingRanges.fire({ owner: ownerId }); + this._onDidUpdateCommentingRanges.fire({ uniqueOwner: ownerId }); } - async toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { - const commentController = this._commentControls.get(owner); + async toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (commentController) { return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None); @@ -427,8 +429,8 @@ export class CommentService extends Disposable implements ICommentService { } } - hasReactionHandler(owner: string): boolean { - const commentProvider = this._commentControls.get(owner); + hasReactionHandler(uniqueOwner: string): boolean { + const commentProvider = this._commentControls.get(uniqueOwner); if (commentProvider) { return !!commentProvider.features.reactionHandler; @@ -447,10 +449,10 @@ export class CommentService extends Disposable implements ICommentService { // This can happen because continue on comments are stored separately from local un-submitted comments. for (const documentCommentThread of documentComments.threads) { if (documentCommentThread.comments?.length === 0 && documentCommentThread.range) { - this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, owner: documentComments.owner }); + this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, uniqueOwner: documentComments.uniqueOwner }); } } - const pendingComments = this._continueOnComments.get(documentComments.owner); + const pendingComments = this._continueOnComments.get(documentComments.uniqueOwner); documentComments.pendingCommentThreads = pendingComments?.filter(pendingComment => pendingComment.uri.toString() === resource.toString()); return documentComments; }) @@ -495,8 +497,8 @@ export class CommentService extends Disposable implements ICommentService { this.storageService.store(CONTINUE_ON_COMMENTS, commentsToSave, StorageScope.WORKSPACE, StorageTarget.USER); } - removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined { - const pendingComments = this._continueOnComments.get(pendingComment.owner); + removeContinueOnComment(pendingComment: { range: IRange; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined { + const pendingComments = this._continueOnComments.get(pendingComment.uniqueOwner); if (pendingComments) { const commentIndex = pendingComments.findIndex(comment => comment.uri.toString() === pendingComment.uri.toString() && Range.equalsRange(comment.range, pendingComment.range) && (pendingComment.isReply === undefined || comment.isReply === pendingComment.isReply)); if (commentIndex > -1) { @@ -509,14 +511,14 @@ export class CommentService extends Disposable implements ICommentService { private _addContinueOnComments(pendingComments: PendingCommentThread[], map: Map): Set { const changedOwners = new Set(); for (const pendingComment of pendingComments) { - if (!map.has(pendingComment.owner)) { - map.set(pendingComment.owner, [pendingComment]); - changedOwners.add(pendingComment.owner); + if (!map.has(pendingComment.uniqueOwner)) { + map.set(pendingComment.uniqueOwner, [pendingComment]); + changedOwners.add(pendingComment.uniqueOwner); } else { - const commentsForOwner = map.get(pendingComment.owner)!; + const commentsForOwner = map.get(pendingComment.uniqueOwner)!; if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range))) { commentsForOwner.push(pendingComment); - changedOwners.add(pendingComment.owner); + changedOwners.add(pendingComment.uniqueOwner); } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index e5ae9040d50..baa3438345d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -105,8 +105,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _contextKeyService: IContextKeyService; private _scopedInstantiationService: IInstantiationService; - public get owner(): string { - return this._owner; + public get uniqueOwner(): string { + return this._uniqueOwner; } public get commentThread(): languages.CommentThread { return this._commentThread; @@ -120,7 +120,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget constructor( editor: ICodeEditor, - private _owner: string, + private _uniqueOwner: string, private _commentThread: languages.CommentThread, private _pendingComment: string | undefined, private _pendingEdits: { [key: number]: string } | undefined, @@ -137,7 +137,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget [IContextKeyService, this._contextKeyService] )); - const controller = this.commentService.getCommentController(this._owner); + const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { this._commentOptions = controller.options; } @@ -229,7 +229,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget CommentThreadWidget, container, this.editor, - this._owner, + this._uniqueOwner, this.editor.getModel()!.uri, this._contextKeyService, this._scopedInstantiationService, @@ -258,7 +258,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { range = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.endColumn); } - await this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, range); + await this.commentService.updateCommentThreadTemplate(this.uniqueOwner, this._commentThread.commentThreadHandle, range); } } }, @@ -281,7 +281,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private deleteCommentThread(): void { this.dispose(); - this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); + this.commentService.disposeCommentThread(this.uniqueOwner, this._commentThread.threadId); } public collapse() { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 35c0cd6eb7f..e83cb560c6c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -203,10 +203,10 @@ class CommentingRangeDecorator { intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1); intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1); } - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); if (!this._lineHasThread(editor, intersectingEmphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1; @@ -215,27 +215,27 @@ class CommentingRangeDecorator { const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine; if (hasBeforeRange) { const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } if (hasAfterRange) { const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) { if (rangeObject.startLineNumber < emphasisLine) { const beforeRange = new Range(range.startLineNumber, 1, emphasisLine - 1, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } const emphasisRange = new Range(emphasisLine, 1, emphasisLine, 1); if (!this._lineHasThread(editor, emphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } if (emphasisLine < rangeObject.endLineNumber) { const afterRange = new Range(emphasisLine + 1, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); } }); } @@ -274,7 +274,7 @@ class CommentingRangeDecorator { return foundInfos.map(foundInfo => { return { action: { - ownerId: foundInfo.owner, + ownerId: foundInfo.uniqueOwner, extensionId: foundInfo.extensionId, label: foundInfo.label, commentingRangesInfo: foundInfo.commentingRanges @@ -290,7 +290,7 @@ class CommentingRangeDecorator { for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); if (range && this.areRangesIntersectingOrTouchingByLine(range, commentRange)) { - // We can have several commenting ranges that match from the same owner because of how + // We can have several commenting ranges that match from the same uniqueOwner because of how // the line hover and selection decoration is done. // The ranges must be merged so that we can see if the new commentRange fits within them. const action = decoration.getCommentAction(); @@ -383,7 +383,7 @@ export class CommentController implements IEditorContribution { private _computeCommentingRangePromise!: CancelablePromise | null; private _computeCommentingRangeScheduler!: Delayer> | null; private _pendingNewCommentCache: { [key: string]: { [key: string]: string } }; - private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // owner -> threadId -> uniqueIdInThread -> pending comment + private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment private _inProcessContinueOnComments: Map = new Map(); private _editorDisposables: IDisposable[] = []; private _activeCursorHasCommentingRange: IContextKey; @@ -496,7 +496,7 @@ export class CommentController implements IEditorContribution { if (pendingNewComment !== lastCommentBody) { pendingComments.push({ - owner: zone.owner, + uniqueOwner: zone.uniqueOwner, uri: zone.editor.getModel()!.uri, range: zone.commentThread.range, body: pendingNewComment, @@ -824,7 +824,7 @@ export class CommentController implements IEditorContribution { await this._computePromise; } - const commentInfo = this._commentInfos.filter(info => info.owner === e.owner); + const commentInfo = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner); if (!commentInfo || !commentInfo.length) { return; } @@ -835,14 +835,14 @@ export class CommentController implements IEditorContribution { const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString()); removed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { const matchedZone = matchedZones[0]; const index = this._commentWidgets.indexOf(matchedZone); this._commentWidgets.splice(index, 1); matchedZone.dispose(); } - const infosThreads = this._commentInfos.filter(info => info.owner === e.owner)[0].threads; + const infosThreads = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads; for (let i = 0; i < infosThreads.length; i++) { if (infosThreads[i] === thread) { infosThreads.splice(i, 1); @@ -852,7 +852,7 @@ export class CommentController implements IEditorContribution { }); changed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); @@ -860,19 +860,19 @@ export class CommentController implements IEditorContribution { } }); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { return; } - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { matchedNewCommentThreadZones[0].update(thread); return; } - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.owner)?.findIndex(pending => { + const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { if (pending.range === undefined) { return thread.range === undefined; } else { @@ -881,14 +881,14 @@ export class CommentController implements IEditorContribution { }); let continueOnCommentText: string | undefined; if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.owner)?.splice(continueOnCommentIndex, 1)[0].body; + continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; } - const pendingCommentText = (this._pendingNewCommentCache[e.owner] && this._pendingNewCommentCache[e.owner][thread.threadId]) + const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.owner] && this._pendingEditsCache[e.owner][thread.threadId]; - this.displayCommentThread(e.owner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); + const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; + this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); } @@ -902,12 +902,12 @@ export class CommentController implements IEditorContribution { } private async resumePendingComment(editorURI: URI, thread: languages.PendingCommentThread) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === thread.owner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === thread.uniqueOwner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); if (thread.isReply && matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: true }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: true }); matchedZones[0].setPendingComment(thread.body); } else if (matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); const existingPendingComment = matchedZones[0].getPendingComments().newComment; // We need to try to reconcile the existing pending comment with the incoming pending comment let pendingComment: string; @@ -920,15 +920,15 @@ export class CommentController implements IEditorContribution { } matchedZones[0].setPendingComment(pendingComment); } else if (!thread.isReply) { - const threadStillAvailable = this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + const threadStillAvailable = this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); if (!threadStillAvailable) { return; } - if (!this._inProcessContinueOnComments.has(thread.owner)) { - this._inProcessContinueOnComments.set(thread.owner, []); + if (!this._inProcessContinueOnComments.has(thread.uniqueOwner)) { + this._inProcessContinueOnComments.set(thread.uniqueOwner, []); } - this._inProcessContinueOnComments.get(thread.owner)?.push(thread); - await this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); + this._inProcessContinueOnComments.get(thread.uniqueOwner)?.push(thread); + await this.commentService.createCommentThreadTemplate(thread.uniqueOwner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); } } @@ -968,7 +968,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { + private displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { const editor = this.editor?.getModel(); if (!editor) { return; @@ -979,9 +979,9 @@ export class CommentController implements IEditorContribution { let continueOnCommentReply: languages.PendingCommentThread | undefined; if (thread.range && !pendingComment) { - continueOnCommentReply = this.commentService.removeContinueOnComment({ owner, uri: editor.uri, range: thread.range, isReply: true }); + continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } - const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); + const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); zoneWidget.display(thread.range); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); @@ -1259,8 +1259,8 @@ export class CommentController implements IEditorContribution { hasCommentingRanges = true; } - const providerCacheStore = this._pendingNewCommentCache[info.owner]; - const providerEditsCacheStore = this._pendingEditsCache[info.owner]; + const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner]; + const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner]; info.threads = info.threads.filter(thread => !thread.isDisposed); info.threads.forEach(thread => { let pendingComment: string | undefined = undefined; @@ -1273,7 +1273,7 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - this.displayCommentThread(info.owner, thread, pendingComment, pendingEdits); + this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); }); for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); @@ -1303,7 +1303,7 @@ export class CommentController implements IEditorContribution { this._commentWidgets.forEach(zone => { const pendingComments = zone.getPendingComments(); const pendingNewComment = pendingComments.newComment; - const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.owner]; + const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.uniqueOwner]; let lastCommentBody; if (zone.commentThread.comments && zone.commentThread.comments.length) { @@ -1316,10 +1316,10 @@ export class CommentController implements IEditorContribution { } if (pendingNewComment && (pendingNewComment !== lastCommentBody)) { if (!providerNewCommentCacheStore) { - this._pendingNewCommentCache[zone.owner] = {}; + this._pendingNewCommentCache[zone.uniqueOwner] = {}; } - this._pendingNewCommentCache[zone.owner][zone.commentThread.threadId] = pendingNewComment; + this._pendingNewCommentCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingNewComment; } else { if (providerNewCommentCacheStore) { delete providerNewCommentCacheStore[zone.commentThread.threadId]; @@ -1327,12 +1327,12 @@ export class CommentController implements IEditorContribution { } const pendingEdits = pendingComments.edits; - const providerEditsCacheStore = this._pendingEditsCache[zone.owner]; + const providerEditsCacheStore = this._pendingEditsCache[zone.uniqueOwner]; if (Object.keys(pendingEdits).length > 0) { if (!providerEditsCacheStore) { - this._pendingEditsCache[zone.owner] = {}; + this._pendingEditsCache[zone.uniqueOwner] = {}; } - this._pendingEditsCache[zone.owner][zone.commentThread.threadId] = pendingEdits; + this._pendingEditsCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingEdits; } else if (providerEditsCacheStore) { delete providerEditsCacheStore[zone.commentThread.threadId]; } diff --git a/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index 6d345350e83..d0701d5f344 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -43,15 +43,15 @@ export class CommentsModel extends Disposable implements ICommentsModel { }); } - public setCommentThreads(owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(owner, commentThreads) }); + public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(uniqueOwner, owner, commentThreads) }); this.updateResourceCommentThreads(); } - public deleteCommentsByOwner(owner?: string): void { - if (owner) { - const existingOwner = this.commentThreadsMap.get(owner); - this.commentThreadsMap.set(owner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); + public deleteCommentsByOwner(uniqueOwner?: string): void { + if (uniqueOwner) { + const existingOwner = this.commentThreadsMap.get(uniqueOwner); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); } else { this.commentThreadsMap.clear(); } @@ -59,9 +59,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { } public updateCommentThreads(event: ICommentThreadChangedEvent): boolean { - const { owner, ownerLabel, removed, changed, added } = event; + const { uniqueOwner, owner, ownerLabel, removed, changed, added } = event; - const threadsForOwner = this.commentThreadsMap.get(owner)?.resourceWithCommentThreads || []; + const threadsForOwner = this.commentThreadsMap.get(uniqueOwner)?.resourceWithCommentThreads || []; removed.forEach(thread => { // Find resource that has the comment thread @@ -91,9 +91,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { // Find comment node on resource that is that thread and replace it const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId); if (index >= 0) { - matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread); + matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread); } else if (thread.comments && thread.comments.length) { - matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread)); + matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread)); } }); @@ -102,14 +102,14 @@ export class CommentsModel extends Disposable implements ICommentsModel { if (existingResource.length) { const resource = existingResource[0]; if (thread.comments && thread.comments.length) { - resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread)); + resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource.resource, thread)); } } else { - threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread])); + threadsForOwner.push(new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(thread.resource!), [thread])); } }); - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); this.updateResourceCommentThreads(); return removed.length > 0 || changed.length > 0 || added.length > 0; @@ -127,11 +127,11 @@ export class CommentsModel extends Disposable implements ICommentsModel { } } - private groupByResource(owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { + private groupByResource(uniqueOwner: string, owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { const resourceCommentThreads: ResourceWithCommentThreads[] = []; const commentThreadsByResource = new Map(); for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) { - commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(owner, URI.parse(group[0].resource!), group)); + commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(group[0].resource!), group)); } commentThreadsByResource.forEach((v, i, m) => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 451bc210789..df131acad4b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -10,7 +10,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentNode, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel'; -import { ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ITreeContextMenuEvent, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -34,6 +34,14 @@ import { ILocalizedString } from 'vs/platform/action/common/action'; import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_STORAGE_ID = 'Comments'; @@ -62,6 +70,7 @@ interface ICommentThreadTemplateData { separator: HTMLElement; timestamp: TimestampWidget; }; + actionBar: ActionBar; disposables: IDisposable[]; } @@ -124,10 +133,59 @@ export class ResourceWithCommentsRenderer implements IListRenderer, ICommentThreadTemplateData> { templateId: string = 'comment-node'; constructor( + private actionViewItemProvider: IActionViewItemProvider, + private menus: CommentsMenus, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService private themeService: IThemeService @@ -137,16 +195,22 @@ export class CommentNodeRenderer implements IListRenderer const threadContainer = dom.append(container, dom.$('.comment-thread-container')); const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container')); + const metadata = dom.append(metadataContainer, dom.$('.comment-metadata')); const threadMetadata = { - icon: dom.append(metadataContainer, dom.$('.icon')), - userNames: dom.append(metadataContainer, dom.$('.user')), - timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), - separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')), - range: dom.append(metadataContainer, dom.$('.range')) + icon: dom.append(metadata, dom.$('.icon')), + userNames: dom.append(metadata, dom.$('.user')), + timestamp: new TimestampWidget(this.configurationService, dom.append(metadata, dom.$('.timestamp-container'))), + separator: dom.append(metadata, dom.$('.separator')), + commentPreview: dom.append(metadata, dom.$('.text')), + range: dom.append(metadata, dom.$('.range')) }; threadMetadata.separator.innerText = '\u00b7'; + const actionsContainer = dom.append(metadataContainer, dom.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider + }); + const snippetContainer = dom.append(threadContainer, dom.$('.comment-snippet-container')); const repliesMetadata = { container: snippetContainer, @@ -158,9 +222,9 @@ export class CommentNodeRenderer implements IListRenderer }; repliesMetadata.separator.innerText = '\u00b7'; repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent)); - const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; - return { threadMetadata, repliesMetadata, disposables }; + const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; + return { threadMetadata, repliesMetadata, actionBar, disposables }; } private getCountString(commentCount: number): string { @@ -198,6 +262,8 @@ export class CommentNodeRenderer implements IListRenderer } renderElement(node: ITreeNode, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + const commentCount = node.element.replies.length + 1; templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); @@ -232,6 +298,14 @@ export class CommentNodeRenderer implements IListRenderer } } + const menuActions = this.menus.getResourceActions(node.element); + templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); + templateData.actionBar.context = { + commentControlHandle: node.element.controllerHandle, + commentThreadHandle: node.element.threadHandle, + $mid: MarshalledId.CommentThread + }; + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -250,6 +324,7 @@ export class CommentNodeRenderer implements IListRenderer disposeTemplate(templateData: ICommentThreadTemplateData): void { templateData.disposables.forEach(disposeable => disposeable.dispose()); + templateData.actionBar.dispose(); } } @@ -347,6 +422,8 @@ export class Filter implements ITreeFilter { + private readonly menus: CommentsMenus; + constructor( labels: ResourceLabels, container: HTMLElement, @@ -355,12 +432,16 @@ export class CommentsList extends WorkbenchObjectTree this.commentsOnContextMenu(e))); + } + + private commentsOnContextMenu(treeEvent: ITreeContextMenuEvent): void { + const node: CommentsModel | ResourceWithCommentThreads | CommentNode | null = treeEvent.element; + if (!(node instanceof CommentNode)) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.setFocus([node]); + const actions = this.menus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + getActions: () => actions, + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.domFocus(); + } + }, + getActionsContext: () => ({ + commentControlHandle: node.controllerHandle, + commentThreadHandle: node.threadHandle, + $mid: MarshalledId.CommentThread + }) + }); } filterComments(): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 385ac16e1dc..2530cf96e7c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -13,7 +13,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { IViewPaneOptions, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -192,10 +191,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this)); this._register(this.commentService.onDidDeleteDataProvider(this.onDataProviderDeleted, this)); - const styleElement = dom.createStyleSheet(container); - this.applyStyles(styleElement); - this._register(this.themeService.onDidColorThemeChange(_ => this.applyStyles(styleElement))); - this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.refresh(); @@ -220,33 +215,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private applyStyles(styleElement: HTMLStyleElement) { - const content: string[] = []; - - const theme = this.themeService.getColorTheme(); - const linkColor = theme.getColor(textLinkForeground); - if (linkColor) { - content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`); - } - - const linkActiveColor = theme.getColor(textLinkActiveForeground); - if (linkActiveColor) { - content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`); - } - - const focusColor = theme.getColor(focusBorder); - if (focusColor) { - content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); - } - - const codeTextForegroundColor = theme.getColor(textPreformatForeground); - if (codeTextForegroundColor) { - content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`); - } - - styleElement.textContent = content.join('\n'); - } - private async renderComments(): Promise { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index a349ec52490..a1132e43d49 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -36,6 +36,11 @@ overflow: hidden; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata { + flex: 1; + display: flex; +} + .comments-panel .count, .comments-panel .user { padding-right: 5px; @@ -117,3 +122,34 @@ .comments-panel .hide { display: none; } + +.comments-panel .comments-panel-container .text a { + color: var(--vscode-textLink-foreground); +} + +.comments-panel .comments-panel-container .text a:hover, +.comments-panel .comments-panel-container a:active { + color: var(--vscode-textLink-activeForeground); +} + +.comments-panel .comments-panel-container .text a:focus { + outline-color: var(--vscode-focusBorder); +} + +.comments-panel .comments-panel-container .text code { + color: var(--vscode-textPreformat-foreground); +} + +.comments-panel .comments-panel-container .actions { + display: none; +} + +.comments-panel .comments-panel-container .actions .action-label { + padding: 2px; +} + +.comments-panel .monaco-list .monaco-list-row:hover .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.selected .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.focused .comment-metadata-container .actions { + display: block; +} diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index 9a6d8786372..e4418a28dfc 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -8,29 +8,26 @@ import { IRange } from 'vs/editor/common/core/range'; import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { + uniqueOwner: string; owner: string; ownerLabel: string; } export class CommentNode { - owner: string; - threadId: string; - range: IRange | undefined; - comment: Comment; + isRoot: boolean = false; replies: CommentNode[] = []; - resource: URI; - isRoot: boolean; - threadState?: CommentThreadState; - constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange | undefined, threadState: CommentThreadState | undefined) { - this.owner = owner; - this.threadId = threadId; - this.comment = comment; - this.resource = resource; - this.range = range; - this.isRoot = false; - this.threadState = threadState; - } + constructor( + public readonly uniqueOwner: string, + public readonly threadId: string, + public readonly resource: URI, + public readonly comment: Comment, + public readonly range: IRange | undefined, + public readonly threadState: CommentThreadState | undefined, + public readonly contextValue: string | undefined, + public readonly owner: string, + public readonly controllerHandle: number, + public readonly threadHandle: number) { } hasReply(): boolean { return this.replies && this.replies.length !== 0; @@ -39,21 +36,23 @@ export class CommentNode { export class ResourceWithCommentThreads { id: string; + uniqueOwner: string; owner: string; ownerLabel: string | undefined; commentThreads: CommentNode[]; // The top level comments on the file. Replys are nested under each node. resource: URI; - constructor(owner: string, resource: URI, commentThreads: CommentThread[]) { + constructor(uniqueOwner: string, owner: string, resource: URI, commentThreads: CommentThread[]) { + this.uniqueOwner = uniqueOwner; this.owner = owner; this.id = resource.toString(); this.resource = resource; - this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(owner, resource, thread)); + this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource, thread)); } - public static createCommentNode(owner: string, resource: URI, commentThread: CommentThread): CommentNode { + public static createCommentNode(uniqueOwner: string, owner: string, resource: URI, commentThread: CommentThread): CommentNode { const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId, resource, comment, range, commentThread.state)); + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(uniqueOwner, threadId, resource, comment, range, commentThread.state, commentThread.contextValue, owner, commentThread.controllerHandle, commentThread.commentThreadHandle)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index a3e171b9d40..cd5f0ddf60c 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -49,6 +49,7 @@ class TestCommentThread implements CommentThread { class TestCommentController implements ICommentController { id: string = 'test'; label: string = 'Test Comments'; + owner: string = 'test'; features = {}; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index bf0fe703109..1c42939cab4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -141,7 +141,7 @@ export class CellComments extends CellContentPart { if (this.notebookEditor.hasModel()) { const commentInfos = coalesce(await this.commentService.getNotebookComments(element.uri)); if (commentInfos.length && commentInfos[0].threads.length) { - return { owner: commentInfos[0].owner, thread: commentInfos[0].threads[0] }; + return { owner: commentInfos[0].uniqueOwner, thread: commentInfos[0].threads[0] }; } } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 31c61213f4d..c43a6eb3bed 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -290,6 +290,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('comment.commentContext', "The contributed comment context menu, rendered as a right click menu on the an individual comment in the comment thread's peek view."), proposed: 'contribCommentPeekContext' }, + { + key: 'commentsView/commentThread/context', + id: MenuId.CommentsViewThreadActions, + description: localize('commentsView.threadActions', "The contributed comment thread context menu in the comments view"), + proposed: 'contribCommentsViewThreadMenus' + }, { key: 'notebook/toolbar', id: MenuId.NotebookToolbar, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 8571e00fc4d..67c26906df3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -27,6 +27,7 @@ export const allApiProposals = Object.freeze({ contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', + contribCommentsViewThreadMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', contribIssueReporter: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts b/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts new file mode 100644 index 00000000000..9dc199c51cc --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * 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 `commentsView/commentThread/context` menu contribution point From 08845e942beb23084e51ffac00f2f40f80a23b95 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 7 Mar 2024 13:40:51 +0100 Subject: [PATCH 027/141] fix #206988 (#207057) --- .../workbench/contrib/extensions/browser/media/extension.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 136ce1af0f7..985b5511c05 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -214,10 +214,6 @@ text-overflow: ellipsis; } -.extension-list-item > .details > .footer > .monaco-action-bar > .actions-container { - flex-wrap: wrap-reverse; -} - .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .action-label:not(.icon) { border-radius: 2px; } From 68f565d32a8248b1c5677426621c34a875c1a8da Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 7 Mar 2024 14:13:08 +0100 Subject: [PATCH 028/141] emit downloaded event in mac (#207052) --- src/vs/platform/menubar/electron-main/menubar.ts | 2 +- src/vs/platform/update/electron-main/updateService.darwin.ts | 2 ++ src/vs/workbench/browser/parts/titlebar/menubarControl.ts | 2 +- src/vs/workbench/contrib/update/browser/update.ts | 5 ++++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 2ab5bcecbfa..f11b38cb19d 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -647,7 +647,7 @@ export class Menubar { return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; case StateType.Downloaded: - return [new MenuItem({ + return isMacintosh ? [] : [new MenuItem({ label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => { this.reportMenuActionTelemetry('InstallUpdate'); this.updateService.applyUpdate(); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 27a2fcbeadf..183c69da906 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -109,6 +109,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } + this.setState(State.Downloaded(update)); + type UpdateDownloadedClassification = { owner: 'joaomoreno'; version: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version number of the new VS Code that has been downloaded.' }; diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 1bfc3526c2e..1453c7d8eeb 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -474,7 +474,7 @@ export class CustomMenubarControl extends MenubarControl { return new Action('update.downloading', localize('DownloadingUpdate', "Downloading Update..."), undefined, false); case StateType.Downloaded: - return new Action('update.install', localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), undefined, true, () => + return isMacintosh ? null : new Action('update.install', localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), undefined, true, () => this.updateService.applyUpdate()); case StateType.Updating: diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 45a8227909b..e1020e6e771 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -17,7 +17,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ReleaseNotesManager } from 'vs/workbench/contrib/update/browser/releaseNotesEditor'; -import { isWeb, isWindows } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -319,6 +319,9 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu // windows fast updates private onUpdateDownloaded(update: IUpdate): void { + if (isMacintosh) { + return; + } if (this.configurationService.getValue('update.enableWindowsBackgroundUpdates') && this.productService.target === 'user') { return; } From 4a564dbe2cfd16f38baedc6f0f47973ab4ef0631 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:14:11 -0800 Subject: [PATCH 029/141] Spread out terminal group names for easier use by exts Fixes #207065 --- .../contrib/terminal/browser/terminalMenus.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 7f119914b09..a000c5e8465 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -20,17 +20,17 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed const enum ContextMenuGroup { Create = '1_create', - Edit = '2_edit', - Clear = '3_clear', - Kill = '4_kill', - Config = '5_config' + Edit = '3_edit', + Clear = '5_clear', + Kill = '7_kill', + Config = '9_config' } export const enum TerminalMenuBarGroup { Create = '1_create', - Run = '2_run', - Manage = '3_manage', - Configure = '4_configure' + Run = '3_run', + Manage = '5_manage', + Configure = '7_configure' } export function setupTerminalMenus(): void { From 75dd427e0970cecaef5ff100e25d62233de2d310 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:38:19 -0800 Subject: [PATCH 030/141] Replace ext suggest command with experimental inline chat keybinding Fixes #4256 --- .../workbench/contrib/terminal/common/terminal.ts | 13 ++++++++++++- .../chat/browser/terminalChatActions.ts | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fc925b646bb..60c5b601e8a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -649,7 +649,18 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ 'workbench.action.quickOpenView', 'workbench.action.toggleMaximizedPanel', 'notification.acceptPrimaryAction', - 'runCommands' + 'runCommands', + 'workbench.action.terminal.chat.start', + 'workbench.action.terminal.chat.close', + 'workbench.action.terminal.chat.discard', + 'workbench.action.terminal.chat.makeRequest', + 'workbench.action.terminal.chat.cancel', + 'workbench.action.terminal.chat.feedbackHelpful', + 'workbench.action.terminal.chat.feedbackUnhelpful', + 'workbench.action.terminal.chat.feedbackReportIssue', + 'workbench.action.terminal.chat.runCommand', + 'workbench.action.terminal.chat.insertCommand', + 'workbench.action.terminal.chat.viewInChat', ]; export const terminalContributionsDescriptor: IExtensionPointDescriptor = { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index ff650600398..7f3ecc81464 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -23,7 +23,8 @@ registerActiveXtermAction({ keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.focusInAny), - weight: KeybindingWeight.WorkbenchContrib, + // HACK: Force weight to be higher than the extension contributed keybinding to override it until it gets replaced + weight: KeybindingWeight.ExternalExtension + 1, // KeybindingWeight.WorkbenchContrib, }, f1: true, category: AbstractInlineChatAction.category, From b1cf638f7de5e7d9c0f57120fb0b6c7c71aa85a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:40:26 -0800 Subject: [PATCH 031/141] Allow using terminal inline chat without focus Part of #4256 --- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index ff650600398..0420ede6250 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -29,7 +29,7 @@ registerActiveXtermAction({ category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.focusInAny), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), // TODO: This needs to change to check for a terminal location capable agent CTX_INLINE_CHAT_HAS_PROVIDER ), From 51883a73b818226db55879262b34bb80d3bcedff Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:44:01 -0800 Subject: [PATCH 032/141] Ensure context keys are created on chat contribution init Fixes #207037 --- .../chat/browser/terminalChatController.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 94a66a2bdd2..cbdd7454d2d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -52,11 +52,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _chatWidget: Lazy | undefined; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } - private readonly _requestActiveContextKey!: IContextKey; - private readonly _terminalAgentRegisteredContextKey!: IContextKey; - private readonly _responseTypeContextKey!: IContextKey; - private readonly _responseSupportsIssueReportingContextKey!: IContextKey; - private readonly _sessionResponseVoteContextKey!: IContextKey; + private readonly _requestActiveContextKey: IContextKey; + private readonly _terminalAgentRegisteredContextKey: IContextKey; + private readonly _responseTypeContextKey: IContextKey; + private readonly _responseSupportsIssueReportingContextKey: IContextKey; + private readonly _sessionResponseVoteContextKey: IContextKey; private _messages = this._store.add(new Emitter()); @@ -90,15 +90,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr ) { super(); - if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { - return; - } this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); this._responseTypeContextKey = TerminalChatContextKeys.responseType.bindTo(this._contextKeyService); this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + if (!this._chatAgentService.getAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { if (this._chatAgentService.getAgent(this._terminalAgentId)) { From 5962739ad59ea0f61d128b48ae01816e2a11edd5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:54:51 -0800 Subject: [PATCH 033/141] Don't get rid of model early, get history from it Fixes #4263 --- .../chat/browser/terminalChatController.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 94a66a2bdd2..bd720107fd0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -24,7 +24,7 @@ import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/te import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; -import { ChatModel, ChatRequestModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; const enum Message { @@ -209,16 +209,19 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey.reset(); } - private updateModel(): void { + async acceptInput(): Promise { const providerInfo = this._chatService.getProviderInfos()?.[0]; if (!providerInfo) { return; } - this._model ??= this._chatService.startSession(providerInfo.id, CancellationToken.None); - } + if (!this._model) { + this._model = this._chatService.startSession(providerInfo.id, CancellationToken.None); + if (!this._model) { + throw new Error('Could not start chat session'); + } + } + const model = this._model; - async acceptInput(): Promise { - this.updateModel(); this._lastInput = this._chatWidget?.rawValue?.input(); if (!this._lastInput) { return; @@ -258,7 +261,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } }; - await this._model?.waitForInitialization(); + await model.waitForInitialization(); const request: IParsedChatRequest = { text: this._lastInput, parts: [] @@ -266,16 +269,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr const requestVarData: IChatRequestVariableData = { variables: [] }; - this._currentRequest = this._model?.addRequest(request, requestVarData); + this._currentRequest = model.addRequest(request, requestVarData); const requestProps: IChatAgentRequest = { - sessionId: this._model!.sessionId, + sessionId: model.sessionId, requestId: this._currentRequest!.id, agentId: this._terminalAgentId, message: this._lastInput, variables: { variables: [] }, }; try { - const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); + const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); From aa2cd708efcb5da1b44720723d977415a38eee55 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:00:14 -0800 Subject: [PATCH 034/141] Use mutable disposable for terminal chat model --- .../chat/browser/terminalChatController.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index bd720107fd0..b523f69eb5d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -7,7 +7,7 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -73,7 +73,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _terminalAgentId = 'terminal'; - private _model: ChatModel | undefined; + private _model: MutableDisposable = new MutableDisposable(); constructor( private readonly _instance: ITerminalInstance, @@ -137,7 +137,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr acceptFeedback(helpful?: boolean): void { const providerId = this._chatService.getProviderInfos()?.[0]?.id; - if (!providerId || !this._currentRequest || !this._model) { + const model = this._model.value; + if (!providerId || !this._currentRequest || !model) { return; } let action: ChatUserAction; @@ -148,7 +149,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; } // TODO:extract into helper method - for (const request of this._model.getRequests()) { + for (const request of model.getRequests()) { if (request.response?.response.value || request.response?.result) { this._chatService.notifyUserAction({ providerId, @@ -165,7 +166,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr cancel(): void { if (this._currentRequest) { - this._model?.cancelRequest(this._currentRequest); + this._model.value?.cancelRequest(this._currentRequest); } this._requestActiveContextKey.set(false); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); @@ -198,10 +199,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr clear(): void { if (this._currentRequest) { - this._model?.cancelRequest(this._currentRequest); + this._model.value?.cancelRequest(this._currentRequest); } - this._model?.dispose(); - this._model = undefined; + this._model.clear(); this._chatWidget?.rawValue?.hide(); this._chatWidget?.rawValue?.setValue(undefined); this._responseTypeContextKey.reset(); @@ -214,13 +214,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!providerInfo) { return; } - if (!this._model) { - this._model = this._chatService.startSession(providerInfo.id, CancellationToken.None); - if (!this._model) { + if (!this._model.value) { + this._model.value = this._chatService.startSession(providerInfo.id, CancellationToken.None); + if (!this._model.value) { throw new Error('Could not start chat session'); } } - const model = this._model; + const model = this._model.value; this._lastInput = this._chatWidget?.rawValue?.input(); if (!this._lastInput) { @@ -241,7 +241,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr responseContent += progress.content; } if (this._currentRequest) { - this._model?.acceptResponseProgress(this._currentRequest, progress); + model.acceptResponseProgress(this._currentRequest, progress); } if (!firstCodeBlock) { const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; @@ -292,7 +292,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); if (this._currentRequest) { - this._model?.completeResponse(this._currentRequest); + model.completeResponse(this._currentRequest); } this._lastResponseContent = responseContent; if (!firstCodeBlock && this._currentRequest) { @@ -344,9 +344,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!providerInfo) { return; } + const model = this._model.value; const widget = await this._chatWidgetService.revealViewForProvider(providerInfo.id); - if (widget && widget.viewModel && this._model) { - for (const request of this._model.getRequests()) { + if (widget && widget.viewModel && model) { + for (const request of model.getRequests()) { if (request.response?.response.value || request.response?.result) { this._chatService.addCompleteRequest(widget.viewModel.sessionId, request.message as IParsedChatRequest, @@ -359,14 +360,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr } } widget.focusLastMessage(); - } else if (!this._model) { + } else if (!model) { widget?.focusInput(); } } override dispose() { if (this._currentRequest) { - this._model?.cancelRequest(this._currentRequest); + this._model.value?.cancelRequest(this._currentRequest); } super.dispose(); this.clear(); From e4825758526ac66de29635f7d18495ac3ed6887a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:04:15 -0800 Subject: [PATCH 035/141] Register model --- .../terminalContrib/chat/browser/terminalChatController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index b523f69eb5d..ce08af1e3f0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -73,7 +73,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _terminalAgentId = 'terminal'; - private _model: MutableDisposable = new MutableDisposable(); + private _model: MutableDisposable = this._register(new MutableDisposable()); constructor( private readonly _instance: ITerminalInstance, From 465e40226b7105d183535fccc61a6c018142f172 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:11:05 -0800 Subject: [PATCH 036/141] Clean up usage of Lazy, prevent double dispose Fixes #207073 --- .../chat/browser/terminalChatController.ts | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 94a66a2bdd2..4ee64950d9e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -39,17 +39,27 @@ const enum Message { } export class TerminalChatController extends Disposable implements ITerminalContribution { - static readonly ID = 'terminal.Chat'; + static readonly ID = 'terminal.chat'; static get(instance: ITerminalInstance): TerminalChatController | null { return instance.getContribution(TerminalChatController.ID); } /** - * Currently focused chat widget. This is used to track action context since - * 'active terminals' are only tracked for non-detached terminal instanecs. + * Currently focused chat widget. This is used to track action context since 'active terminals' + * are only tracked for non-detached terminal instanecs. */ static activeChatWidget?: TerminalChatController; + + /** + * The chat widget for the controller, this is lazy as we don't want to instantiate it until + * both it's required and xterm is ready. + */ private _chatWidget: Lazy | undefined; + + /** + * The chat widget for the controller, this will be undefined if xterm is not ready yet (ie. the + * terminal is still initializing). + */ get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } private readonly _requestActiveContextKey!: IContextKey; @@ -115,7 +125,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatWidget = new Lazy(() => { - const chatWidget = this._register(this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance)); this._register(chatWidget.focusTracker.onDidFocus(() => { TerminalChatController.activeChatWidget = this; @@ -130,7 +139,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._instance.domElement) { throw new Error('FindWidget expected terminal DOM to be initialized'); } - return chatWidget; }); } @@ -160,7 +168,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } } - this._chatWidget?.rawValue?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); + this._chatWidget?.value.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); } cancel(): void { @@ -168,15 +176,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._model?.cancelRequest(this._currentRequest); } this._requestActiveContextKey.set(false); - this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); - this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); - this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._chatWidget?.value.inlineChatWidget.updateProgress(false); + this._chatWidget?.value.inlineChatWidget.updateInfo(''); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); } private _forcedPlaceholder: string | undefined = undefined; private _updatePlaceholder(): void { - const inlineChatWidget = this._chatWidget?.rawValue?.inlineChatWidget; + const inlineChatWidget = this._chatWidget?.value.inlineChatWidget; if (inlineChatWidget) { inlineChatWidget.placeholder = this._getPlaceholderText(); } @@ -202,8 +210,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._model?.dispose(); this._model = undefined; - this._chatWidget?.rawValue?.hide(); - this._chatWidget?.rawValue?.setValue(undefined); + this._chatWidget?.value.hide(); + this._chatWidget?.value.setValue(undefined); this._responseTypeContextKey.reset(); this._sessionResponseVoteContextKey.reset(); this._requestActiveContextKey.reset(); @@ -219,7 +227,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr async acceptInput(): Promise { this.updateModel(); - this._lastInput = this._chatWidget?.rawValue?.input(); + this._lastInput = this._chatWidget?.value.input(); if (!this._lastInput) { return; } @@ -248,10 +256,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr firstCodeBlock = match?.groups?.content.trim(); shellType = match?.groups?.language; if (firstCodeBlock) { - this._chatWidget?.rawValue?.renderTerminalCommand(firstCodeBlock, shellType); + this._chatWidget?.value.renderTerminalCommand(firstCodeBlock, shellType); this._chatAccessibilityService.acceptResponse(firstCodeBlock, accessibilityRequestId); this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); - this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } } @@ -276,27 +284,27 @@ export class TerminalChatController extends Disposable implements ITerminalContr }; try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); - this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); - this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); - this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); - this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(localize('thinking', "Thinking\u2026")); + this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); + this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined); + this._chatWidget?.value.inlineChatWidget.updateProgress(true); + this._chatWidget?.value.inlineChatWidget.updateInfo(localize('thinking', "Thinking\u2026")); await task; } catch (e) { } finally { this._requestActiveContextKey.set(false); - this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); - this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); - this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._chatWidget?.value.inlineChatWidget.updateProgress(false); + this._chatWidget?.value.inlineChatWidget.updateInfo(''); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); if (this._currentRequest) { this._model?.completeResponse(this._currentRequest); } this._lastResponseContent = responseContent; if (!firstCodeBlock && this._currentRequest) { this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); - this._chatWidget?.rawValue?.renderMessage(responseContent, this._currentRequest.id); + this._chatWidget?.value.renderMessage(responseContent, this._currentRequest.id); this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); - this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; @@ -307,7 +315,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } updateInput(text: string, selectAll = true): void { - const widget = this._chatWidget?.rawValue?.inlineChatWidget; + const widget = this._chatWidget?.value.inlineChatWidget; if (widget) { widget.value = text; if (selectAll) { @@ -317,23 +325,23 @@ export class TerminalChatController extends Disposable implements ITerminalContr } getInput(): string { - return this._chatWidget?.rawValue?.input() ?? ''; + return this._chatWidget?.value.input() ?? ''; } focus(): void { - this._chatWidget?.rawValue?.focus(); + this._chatWidget?.value.focus(); } hasFocus(): boolean { - return !!this._chatWidget?.rawValue?.hasFocus(); + return !!this._chatWidget?.value.hasFocus(); } acceptCommand(shouldExecute: boolean): void { - this._chatWidget?.rawValue?.acceptCommand(shouldExecute); + this._chatWidget?.value.acceptCommand(shouldExecute); } reveal(): void { - this._chatWidget?.rawValue?.reveal(); + this._chatWidget?.value.reveal(); } async viewInChat(): Promise { @@ -367,6 +375,5 @@ export class TerminalChatController extends Disposable implements ITerminalContr } super.dispose(); this.clear(); - this._chatWidget?.rawValue?.dispose(); } } From 9a7ce5b4e74ab4ee7e225f5024c51ade14c65818 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 7 Mar 2024 15:21:06 +0100 Subject: [PATCH 037/141] voice - implement lazy activation (Voice: figure out usage numbers (microsoft/vscode-internalbacklog#4877)) (#207060) * voice - implement lazy activation (Voice: figure out usage numbers (microsoft/vscode-internalbacklog#4877)) * . * . --- .../browser/accessibilityConfiguration.ts | 2 +- .../contrib/chat/common/voiceChat.ts | 6 +- .../actions/voiceChatActions.ts | 10 +- .../chat/test/common/voiceChat.test.ts | 41 ++++---- .../browser/dictation/editorDictation.ts | 4 +- .../electron-sandbox/inlineChatQuickVoice.ts | 4 +- .../speech/browser/speech.contribution.ts | 2 +- .../contrib/speech/browser/speechService.ts | 99 ++++++++++++++++--- .../contrib/speech/common/speechService.ts | 5 +- .../contrib/terminal/browser/terminalVoice.ts | 4 +- 10 files changed, 122 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 5151b2869d9..d9eb9ac7e40 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -695,7 +695,7 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen ) { super(); - this._register(Event.runAndSubscribe(speechService.onDidRegisterSpeechProvider, () => this.updateConfiguration())); + this._register(Event.runAndSubscribe(speechService.onDidChangeHasSpeechProvider, () => this.updateConfiguration())); } private updateConfiguration(): void { diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChat.ts index 5f7208cdd79..1c93007b8cf 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChat.ts @@ -30,7 +30,7 @@ export interface IVoiceChatService { * if the user says "at workspace slash fix this problem", the result * will be "@workspace /fix this problem". */ - createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): IVoiceChatSession; + createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise; } export interface IVoiceChatTextEvent extends ISpeechToTextEvent { @@ -114,7 +114,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { } } - createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): IVoiceChatSession { + async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { const disposables = new DisposableStore(); disposables.add(token.onCancellationRequested(() => disposables.dispose())); @@ -122,7 +122,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { let detectedSlashCommand = false; const emitter = disposables.add(new Emitter()); - const session = this.speechService.createSpeechToTextSession(token, 'chat'); + const session = await this.speechService.createSpeechToTextSession(token, 'chat'); const phrases = this.createPhrases(options.model); disposables.add(session.onDidChange(e => { 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 47909f02faa..aa1fad15b1d 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -314,7 +314,7 @@ class VoiceChatSessions { @IConfigurationService private readonly configurationService: IConfigurationService ) { } - start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): IVoiceChatSession { + async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { this.stop(); let disableTimeout = false; @@ -339,7 +339,7 @@ class VoiceChatSessions { this.voiceChatGettingReadyKey.set(true); - const voiceChatSession = this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); + const voiceChatSession = await this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); let inputValue = controller.getInput(); @@ -474,7 +474,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor return; } - const session = VoiceChatSessions.getInstance(instantiationService).start(controller, context); + const session = await VoiceChatSessions.getInstance(instantiationService).start(controller, context); await holdMode; handle.dispose(); @@ -545,7 +545,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const handle = disposableTimeout(async () => { const controller = await VoiceChatSessionControllerFactory.create(accessor, 'view'); if (controller) { - session = VoiceChatSessions.getInstance(instantiationService).start(controller, context); + session = await VoiceChatSessions.getInstance(instantiationService).start(controller, context); session.setTimeoutDisabled(true); } }, VOICE_KEY_HOLD_THRESHOLD); @@ -921,7 +921,7 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe } private registerListeners(): void { - this._register(Event.runAndSubscribe(this.speechService.onDidRegisterSpeechProvider, () => { + this._register(Event.runAndSubscribe(this.speechService.onDidChangeHasSpeechProvider, () => { this.updateConfiguration(); this.handleKeywordActivation(); })); diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index 391561501ef..419146d5ba1 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -63,8 +63,7 @@ suite('VoiceChat', () => { class TestSpeechService implements ISpeechService { _serviceBrand: undefined; - onDidRegisterSpeechProvider = Event.None; - onDidUnregisterSpeechProvider = Event.None; + onDidChangeHasSpeechProvider = Event.None; readonly hasSpeechProvider = true; readonly hasActiveSpeechToTextSession = false; @@ -74,7 +73,7 @@ suite('VoiceChat', () => { onDidStartSpeechToTextSession = Event.None; onDidEndSpeechToTextSession = Event.None; - createSpeechToTextSession(token: CancellationToken): ISpeechToTextSession { + async createSpeechToTextSession(token: CancellationToken): Promise { return { onDidChange: emitter.event }; @@ -91,10 +90,10 @@ suite('VoiceChat', () => { let service: VoiceChatService; let event: IVoiceChatTextEvent | undefined; - function createSession(options: IVoiceChatSessionOptions) { + async function createSession(options: IVoiceChatSessionOptions) { const cts = new CancellationTokenSource(); disposables.add(toDisposable(() => cts.dispose(true))); - const session = service.createVoiceChatSession(cts.token, options); + const session = await service.createVoiceChatSession(cts.token, options); disposables.add(session.onDidChange(e => { event = e; })); @@ -110,17 +109,17 @@ suite('VoiceChat', () => { }); test('Agent and slash command detection (useAgents: false)', async () => { - testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel }); + await testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel }); }); test('Agent and slash command detection (useAgents: true)', async () => { - testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel }); + await testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel }); }); - function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) { + async function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) { // Nothing to detect - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Started }); assert.strictEqual(event?.status, SpeechToTextStatus.Started); @@ -141,7 +140,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, undefined); // Agent - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -168,7 +167,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent with punctuation - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -180,7 +179,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help'); assert.strictEqual(event?.waitingForInput, false); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At Workspace. help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -193,7 +192,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Slash Command - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Slash fix' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -206,7 +205,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, true); // Agent + Slash Command - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code slash search help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -219,7 +218,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent + Slash Command with punctuation - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code, slash search, help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -231,7 +230,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help'); assert.strictEqual(event?.waitingForInput, false); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code. slash, search help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -244,7 +243,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent not detected twice - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, for at workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -258,7 +257,7 @@ suite('VoiceChat', () => { // Slash command detected after agent recognized if (options.usesAgents) { - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognized); @@ -280,7 +279,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, '/fix'); assert.strictEqual(event?.waitingForInput, true); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognized); @@ -297,7 +296,7 @@ suite('VoiceChat', () => { test('waiting for input', async () => { // Agent - createSession({ usesAgents: true, model: {} as IChatModel }); + await createSession({ usesAgents: true, model: {} as IChatModel }); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -310,7 +309,7 @@ suite('VoiceChat', () => { assert.strictEqual(event.waitingForInput, true); // Slash Command - createSession({ usesAgents: true, model: {} as IChatModel }); + await createSession({ usesAgents: true, model: {} as IChatModel }); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace slash explain' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index c1263ccce50..91727c3a14c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -193,7 +193,7 @@ export class EditorDictation extends Disposable implements IEditorContribution { super(); } - start() { + async start(): Promise { const disposables = new DisposableStore(); this.sessionDisposables.value = disposables; @@ -249,7 +249,7 @@ export class EditorDictation extends Disposable implements IEditorContribution { const cts = new CancellationTokenSource(); disposables.add(toDisposable(() => cts.dispose(true))); - const session = this.speechService.createSpeechToTextSession(cts.token); + const session = await this.speechService.createSpeechToTextSession(cts.token); disposables.add(session.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts index b75bb758c6c..7f24f989723 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts @@ -226,7 +226,7 @@ export class InlineChatQuickVoice implements IEditorContribution { this._store.dispose(); } - start() { + async start() { this._finishCallback?.(true); @@ -236,7 +236,7 @@ export class InlineChatQuickVoice implements IEditorContribution { let message: string | undefined; let preview: string | undefined; - const session = this._voiceChatService.createVoiceChatSession(cts.token, { usesAgents: false }); + const session = await this._voiceChatService.createVoiceChatSession(cts.token, { usesAgents: false }); const listener = session.onDidChange(e => { if (cts.token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/speech/browser/speech.contribution.ts b/src/vs/workbench/contrib/speech/browser/speech.contribution.ts index 03a6035fb80..7184018cd98 100644 --- a/src/vs/workbench/contrib/speech/browser/speech.contribution.ts +++ b/src/vs/workbench/contrib/speech/browser/speech.contribution.ts @@ -7,4 +7,4 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService'; import { SpeechService } from 'vs/workbench/contrib/speech/browser/speechService'; -registerSingleton(ISpeechService, SpeechService, InstantiationType.Delayed); +registerSingleton(ISpeechService, SpeechService, InstantiationType.Eager /* Reads Extension Points */); diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 36186b1458e..d24c324812b 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { firstOrDefault } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,20 +16,49 @@ import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSessio import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export interface ISpeechProviderDescriptor { + readonly name: string; + readonly description?: string; +} + +const speechProvidersExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'speechProviders', + jsonSchema: { + description: localize('vscode.extension.contributes.speechProvider', 'Contributes a Speech Provider'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('speechProviderName', "Unique name for this Speech Provider."), + type: 'string' + }, + description: { + description: localize('speechProviderDescription', "A description of this Speech Provider, shown in the UI."), + type: 'string' + } + } + } + } +}); export class SpeechService extends Disposable implements ISpeechService { readonly _serviceBrand: undefined; - private readonly _onDidRegisterSpeechProvider = this._register(new Emitter()); - readonly onDidRegisterSpeechProvider = this._onDidRegisterSpeechProvider.event; + private readonly _onDidChangeHasSpeechProvider = this._register(new Emitter()); + readonly onDidChangeHasSpeechProvider = this._onDidChangeHasSpeechProvider.event; - private readonly _onDidUnregisterSpeechProvider = this._register(new Emitter()); - readonly onDidUnregisterSpeechProvider = this._onDidUnregisterSpeechProvider.event; - - get hasSpeechProvider() { return this.providers.size > 0; } + get hasSpeechProvider() { return this.providerDescriptors.size > 0 || this.providers.size > 0; } private readonly providers = new Map(); + private readonly providerDescriptors = new Map(); private readonly hasSpeechProviderContext = HasSpeechProvider.bindTo(this.contextKeyService); @@ -38,9 +68,34 @@ export class SpeechService extends Disposable implements ISpeechService { @IHostService private readonly hostService: IHostService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(); + + this.handleAndRegisterSpeechExtensions(); + } + + private handleAndRegisterSpeechExtensions(): void { + speechProvidersExtensionPoint.setHandler((extensions, delta) => { + const oldHasSpeechProvider = this.hasSpeechProvider; + + for (const extension of delta.removed) { + for (const descriptor of extension.value) { + this.providerDescriptors.delete(descriptor.name); + } + } + + for (const extension of delta.added) { + for (const descriptor of extension.value) { + this.providerDescriptors.set(descriptor.name, descriptor); + } + } + + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); + } + }); } registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { @@ -48,21 +103,31 @@ export class SpeechService extends Disposable implements ISpeechService { throw new Error(`Speech provider with identifier ${identifier} is already registered.`); } - this.providers.set(identifier, provider); - this.hasSpeechProviderContext.set(true); + const oldHasSpeechProvider = this.hasSpeechProvider; - this._onDidRegisterSpeechProvider.fire(provider); + this.providers.set(identifier, provider); + + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); + } return toDisposable(() => { - this.providers.delete(identifier); - this._onDidUnregisterSpeechProvider.fire(provider); + const oldHasSpeechProvider = this.hasSpeechProvider; - if (this.providers.size === 0) { - this.hasSpeechProviderContext.set(false); + this.providers.delete(identifier); + + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); } }); } + private handleHasSpeechProviderChange(): void { + this.hasSpeechProviderContext.set(this.hasSpeechProvider); + + this._onDidChangeHasSpeechProvider.fire(); + } + private readonly _onDidStartSpeechToTextSession = this._register(new Emitter()); readonly onDidStartSpeechToTextSession = this._onDidStartSpeechToTextSession.event; @@ -74,7 +139,11 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); - createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): ISpeechToTextSession { + async createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): Promise { + + // Send out extension activation to ensure providers can register + await this.extensionService.activateByEvent('onSpeech'); + const provider = firstOrDefault(Array.from(this.providers.values())); if (!provider) { throw new Error(`No Speech provider is registered.`); diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index e1bbf0f4f8e..6fdd365f39c 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -68,8 +68,7 @@ export interface ISpeechService { readonly _serviceBrand: undefined; - readonly onDidRegisterSpeechProvider: Event; - readonly onDidUnregisterSpeechProvider: Event; + readonly onDidChangeHasSpeechProvider: Event; readonly hasSpeechProvider: boolean; @@ -84,7 +83,7 @@ export interface ISpeechService { * Starts to transcribe speech from the default microphone. The returned * session object provides an event to subscribe for transcribed text. */ - createSpeechToTextSession(token: CancellationToken, context?: string): ISpeechToTextSession; + createSpeechToTextSession(token: CancellationToken, context?: string): Promise; readonly onDidStartKeywordRecognition: Event; readonly onDidEndKeywordRecognition: Event; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts b/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts index 9ff52e29fc5..795b1a7de3f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts @@ -77,7 +77,7 @@ export class TerminalVoiceSession extends Disposable { this._disposables = this._register(new DisposableStore()); } - start(): void { + async start(): Promise { this.stop(); let voiceTimeout = this.configurationService.getValue(AccessibilityVoiceSettingId.SpeechTimeout); if (!isNumber(voiceTimeout) || voiceTimeout < 0) { @@ -89,7 +89,7 @@ export class TerminalVoiceSession extends Disposable { }, voiceTimeout)); this._cancellationTokenSource = new CancellationTokenSource(); this._register(toDisposable(() => this._cancellationTokenSource?.dispose(true))); - const session = this._speechService.createSpeechToTextSession(this._cancellationTokenSource?.token); + const session = await this._speechService.createSpeechToTextSession(this._cancellationTokenSource?.token); this._disposables.add(session.onDidChange((e) => { if (this._cancellationTokenSource?.token.isCancellationRequested) { From 855c3e38523689e77d45d3ed786222d27ad325a4 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 7 Mar 2024 15:22:27 +0100 Subject: [PATCH 038/141] Bump distro (#207074) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2afb762ff12..24bb6558dee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "4623345215aabf2cde23e144a9d4d3ef7803360e", + "distro": "9cacfa07a0a807c640d14bda9eac544fd3295a0f", "author": { "name": "Microsoft Corporation" }, From a01eacaac2e41d1b5faa9ba432b5f0799cc99f88 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 7 Mar 2024 15:31:54 +0100 Subject: [PATCH 039/141] rename and migration --- src/vs/workbench/browser/layout.ts | 2 +- .../parts/activitybar/activitybarPart.ts | 14 +++++++------- .../parts/auxiliarybar/auxiliaryBarPart.ts | 6 +++--- .../browser/parts/sidebar/sidebarPart.ts | 6 +++--- .../browser/workbench.contribution.ts | 19 +++++++++++++++---- .../services/layout/browser/layoutService.ts | 2 +- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 4eac33fbb1a..ad210e00396 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2707,7 +2707,7 @@ class LayoutStateModel extends Disposable { if (oldValue !== undefined) { return !oldValue; } - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.SIDE; + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT; } private setRuntimeValueAndFire(key: RuntimeStateKey, value: T): void { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 2fc1c87d42f..4ee5b07e211 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -378,26 +378,26 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.activityBarLocation.side', + id: 'workbench.action.activityBarLocation.default', title: { - ...localize2('positionActivityBarSide', 'Move Activity Bar to Side'), - mnemonicTitle: localize({ key: 'miSideActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Side"), + ...localize2('positionActivityBarDefault', 'Move Activity Bar to Side'), + mnemonicTitle: localize({ key: 'miDefaultActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Default"), }, - shortTitle: localize('side', "Side"), + shortTitle: localize('default', "Default"), category: Categories.View, - toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), + toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), menu: [{ id: MenuId.ActivityBarPositionMenu, order: 2 }, { id: MenuId.CommandPalette, - when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), + when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), }] }); } run(accessor: ServicesAccessor): void { const configurationService = accessor.get(IConfigurationService); - configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.SIDE); + configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.DEFAULT); } }); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 675f19df25e..895b0ab0a6e 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -206,11 +206,11 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { protected getCompositeBarPosition(): CompositeBarPosition { const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); switch (activityBarPosition) { + case ActivityBarPosition.TOP: return CompositeBarPosition.TOP; case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; case ActivityBarPosition.HIDDEN: return CompositeBarPosition.TITLE; - case ActivityBarPosition.SIDE: return CompositeBarPosition.TITLE; - case ActivityBarPosition.TOP: - default: return CompositeBarPosition.TOP; + case ActivityBarPosition.DEFAULT: return CompositeBarPosition.TITLE; + default: return CompositeBarPosition.TITLE; } } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index b6beaac649d..73734d5dad5 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -212,7 +212,7 @@ export class SidebarPart extends AbstractPaneCompositePart { case ActivityBarPosition.TOP: return CompositeBarPosition.TOP; case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; case ActivityBarPosition.HIDDEN: - case ActivityBarPosition.SIDE: + case ActivityBarPosition.DEFAULT: // noop default: return CompositeBarPosition.TITLE; } } @@ -227,9 +227,9 @@ export class SidebarPart extends AbstractPaneCompositePart { private getRememberedActivityBarVisiblePosition(): ActivityBarPosition { const activityBarPosition = this.storageService.get(LayoutSettings.ACTIVITY_BAR_LOCATION, StorageScope.PROFILE); switch (activityBarPosition) { - case ActivityBarPosition.SIDE: return ActivityBarPosition.SIDE; + case ActivityBarPosition.TOP: return ActivityBarPosition.TOP; case ActivityBarPosition.BOTTOM: return ActivityBarPosition.BOTTOM; - default: return ActivityBarPosition.TOP; + default: return ActivityBarPosition.DEFAULT; } } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index f7e95d8343a..6ed828c45d6 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -497,11 +497,11 @@ const registry = Registry.as(ConfigurationExtensions.Con }, [LayoutSettings.ACTIVITY_BAR_LOCATION]: { 'type': 'string', - 'enum': ['side', 'top', 'bottom', 'hidden'], - 'default': 'side', - 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `side` or `top` / `bottom` of the Primary and Secondary Side Bar or `hidden`."), + 'enum': ['default', 'top', 'bottom', 'hidden'], + 'default': 'default', + 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `default` or `top` / `bottom` of the Primary and Secondary Side Bar or `hidden`."), 'enumDescriptions': [ - localize('workbench.activityBar.location.side', "Show the Activity Bar of the Primary Side Bar on the side."), + localize('workbench.activityBar.location.default', "Show the Activity Bar of the Primary Side Bar on the side."), localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bar."), localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bar."), localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bar.") @@ -811,6 +811,17 @@ Registry.as(Extensions.ConfigurationMigration) } }]); +Registry.as(Extensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: LayoutSettings.ACTIVITY_BAR_LOCATION, migrateFn: (value: any) => { + const results: ConfigurationKeyValuePairs = []; + if (value === 'side') { + results.push([LayoutSettings.ACTIVITY_BAR_LOCATION, { value: ActivityBarPosition.DEFAULT }]); + } + return results; + } + }]); + Registry.as(Extensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'workbench.editor.doubleClickTabToToggleEditorGroupSizes', migrateFn: (value: any) => { diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index b25b1a66c14..321727120df 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -48,7 +48,7 @@ export const enum LayoutSettings { } export const enum ActivityBarPosition { - SIDE = 'side', + DEFAULT = 'default', TOP = 'top', BOTTOM = 'bottom', HIDDEN = 'hidden' From 78f2051e25d020b34a1480d3bc08249a88064a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Cie=C5=9Blak?= Date: Thu, 7 Mar 2024 16:39:59 +0100 Subject: [PATCH 040/141] Inline Edit - make sure we finalize accepting before requesting new edit (#206525) * Inline Edit - make sure we finalize accepting before requesting new edit --- .../contrib/inlineEdit/browser/commands.ts | 4 +-- .../browser/inlineEditController.ts | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/contrib/inlineEdit/browser/commands.ts b/src/vs/editor/contrib/inlineEdit/browser/commands.ts index 11d6dfa2f22..d7e1f4fe8cb 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/commands.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/commands.ts @@ -37,7 +37,7 @@ export class AcceptInlineEdit extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineEditController.get(editor); - controller?.accept(); + await controller?.accept(); } } @@ -147,7 +147,7 @@ export class RejectInlineEdit extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineEditController.get(editor); - controller?.clear(); + await controller?.clear(); } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 646c6f20c22..4e0c10eb335 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent } from 'vs/base/common/observable'; +import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -21,6 +21,7 @@ import { InlineEditHintsWidget } from 'vs/editor/contrib/inlineEdit/browser/inli import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createStyleSheet2 } from 'vs/base/browser/dom'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; export class InlineEditWidget implements IDisposable { constructor(public readonly widget: GhostTextWidget, public readonly edit: IInlineEdit) { } @@ -49,7 +50,7 @@ export class InlineEditController extends Disposable { private _currentRequestCts: CancellationTokenSource | undefined; private _jumpBackPosition: Position | undefined; - private _isAccepting: boolean = false; + private _isAccepting: ISettableObservable = observableValue(this, false); private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); @@ -76,6 +77,9 @@ export class InlineEditController extends Disposable { return; } modelChangedSignal.read(reader); + if (this._isAccepting.read(reader)) { + return; + } this.getInlineEdit(editor, true); })); @@ -111,7 +115,7 @@ export class InlineEditController extends Disposable { //Clear suggestions on lost focus const editorBlurSingal = observableSignalFromEvent('InlineEditController.editorBlurSignal', editor.onDidBlurEditorWidget); - this._register(autorun(reader => { + this._register(autorun(async reader => { /** @description InlineEditController.editorBlur */ if (!this._enabled.read(reader)) { return; @@ -123,7 +127,7 @@ export class InlineEditController extends Disposable { } this._currentRequestCts?.dispose(true); this._currentRequestCts = undefined; - this.clear(false); + await this.clear(false); })); //Invoke provider on focus @@ -222,8 +226,7 @@ export class InlineEditController extends Disposable { private async getInlineEdit(editor: ICodeEditor, auto: boolean) { this._isCursorAtInlineEditContext.set(false); - this.clear(); - this._isAccepting = false; + await this.clear(); const edit = await this.fetchInlineEdit(editor, auto); if (!edit) { return; @@ -254,8 +257,8 @@ export class InlineEditController extends Disposable { this.editor.revealPositionInCenterIfOutsideViewport(this._jumpBackPosition); } - public accept(): void { - this._isAccepting = true; + public async accept() { + this._isAccepting.set(true, undefined); const data = this._currentEdit.get()?.edit; if (!data) { return; @@ -269,10 +272,15 @@ export class InlineEditController extends Disposable { this.editor.pushUndoStop(); this.editor.executeEdits('acceptCurrent', [EditOperation.replace(Range.lift(data.range), text)]); if (data.accepted) { - this._commandService.executeCommand(data.accepted.id, ...data.accepted.arguments || []); + await this._commandService + .executeCommand(data.accepted.id, ...(data.accepted.arguments || [])) + .then(undefined, onUnexpectedExternalError); } this.freeEdit(data); - this._currentEdit.set(undefined, undefined); + transaction((tx) => { + this._currentEdit.set(undefined, tx); + this._isAccepting.set(false, tx); + }); } public jumpToCurrent(): void { @@ -288,10 +296,12 @@ export class InlineEditController extends Disposable { this.editor.revealPositionInCenterIfOutsideViewport(position); } - public clear(sendRejection: boolean = true) { + public async clear(sendRejection: boolean = true) { const edit = this._currentEdit.get()?.edit; - if (edit && edit?.rejected && !this._isAccepting && sendRejection) { - this._commandService.executeCommand(edit.rejected.id, ...edit.rejected.arguments || []); + if (edit && edit?.rejected && sendRejection) { + await this._commandService + .executeCommand(edit.rejected.id, ...(edit.rejected.arguments || [])) + .then(undefined, onUnexpectedExternalError); } if (edit) { this.freeEdit(edit); From 757b6b2556c08f0cc0774aa10a4d9d32606fd810 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Sun, 18 Feb 2024 12:40:24 +0200 Subject: [PATCH 041/141] Tunnel: Add unit tests for port mapping --- .../tunnel/test/common/tunnel.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/vs/platform/tunnel/test/common/tunnel.test.ts diff --git a/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts new file mode 100644 index 00000000000..4884acb05d4 --- /dev/null +++ b/src/vs/platform/tunnel/test/common/tunnel.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + + +suite('Tunnel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function portMappingDoTest(uri: string, + expectedAddress?: string, + expectedPort?: number) { + const res = extractLocalHostUriMetaDataForPortMapping(URI.parse(uri)); + assert.strictEqual(!expectedAddress, !res); + assert.strictEqual(res?.address, expectedAddress); + assert.strictEqual(res?.port, expectedPort); + } + + function portMappingTest(uri: string, expectedAddress?: string, expectedPort?: number) { + portMappingDoTest(uri, expectedAddress, expectedPort); + } + + test('portMapping', () => { + portMappingTest('file:///foo.bar/baz'); + portMappingTest('http://foo.bar:1234'); + portMappingTest('http://localhost:8080', 'localhost', 8080); + portMappingTest('https://localhost:443', 'localhost', 443); + portMappingTest('http://127.0.0.1:3456', '127.0.0.1', 3456); + portMappingTest('http://0.0.0.0:7654', '0.0.0.0', 7654); + portMappingTest('http://localhost:8080/path?foo=bar', 'localhost', 8080); + portMappingTest('http://localhost:8080/path?foo=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8080); + }); +}); From 84e5f9a43a03e97929749f41fa15a025f837bd24 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Sun, 18 Feb 2024 12:45:46 +0200 Subject: [PATCH 042/141] Window: Factor out resolveExternalUri ...and add some unit tests. --- src/vs/workbench/electron-sandbox/window.ts | 92 ++++++++++--------- .../electron-sandbox/resolveExternal.test.ts | 71 ++++++++++++++ 2 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index eb2159272be..946e3a9ad5e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -42,11 +42,11 @@ import { coalesce } from 'vs/base/common/arrays'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; -import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener'; +import { IOpenerService, IResolvedExternalUri, OpenOptions } from 'vs/platform/opener/common/opener'; import { Schemas } from 'vs/base/common/network'; import { INativeHostService } from 'vs/platform/native/common/native'; import { posix } from 'vs/base/common/path'; -import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; +import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; import { IWorkbenchLayoutService, Parts, positionFromString, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -784,6 +784,53 @@ export class NativeWindow extends BaseWindow { }); } + private async openTunnel(address: string, port: number): Promise { + const remoteAuthority = this.environmentService.remoteAuthority; + const addressProvider: IAddressProvider | undefined = remoteAuthority ? { + getAddress: async (): Promise => { + return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; + } + } : undefined; + const tunnel = await this.tunnelService.getExistingTunnel(address, port); + if (!tunnel || (typeof tunnel === 'string')) { + return this.tunnelService.openTunnel(addressProvider, address, port); + } + return tunnel; + } + + async resolveExternalUri(uri: URI, options?: OpenOptions): Promise { + if (options?.allowTunneling) { + const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); + if (portMappingRequest) { + const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port); + if (tunnel && (typeof tunnel !== 'string')) { + const addressAsUri = URI.parse(tunnel.localAddress); + const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: tunnel.localAddress }); + return { + resolved, + dispose: () => tunnel.dispose(), + }; + } + } + } + + if (!options?.openExternal) { + const canHandleResource = await this.fileService.canHandleResource(uri); + if (canHandleResource) { + return { + resolved: URI.from({ + scheme: this.productService.urlProtocol, + path: 'workspace', + query: uri.toString() + }), + dispose() { } + }; + } + } + + return undefined; + } + private setupOpenHandlers(): void { // Handle external open() calls @@ -805,46 +852,7 @@ export class NativeWindow extends BaseWindow { // Register external URI resolver this.openerService.registerExternalUriResolver({ resolveExternalUri: async (uri: URI, options?: OpenOptions) => { - if (options?.allowTunneling) { - const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); - if (portMappingRequest) { - const remoteAuthority = this.environmentService.remoteAuthority; - const addressProvider: IAddressProvider | undefined = remoteAuthority ? { - getAddress: async (): Promise => { - return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; - } - } : undefined; - let tunnel = await this.tunnelService.getExistingTunnel(portMappingRequest.address, portMappingRequest.port); - if (!tunnel || (typeof tunnel === 'string')) { - tunnel = await this.tunnelService.openTunnel(addressProvider, portMappingRequest.address, portMappingRequest.port); - } - if (tunnel && (typeof tunnel !== 'string')) { - const constTunnel = tunnel; - const addressAsUri = URI.parse(constTunnel.localAddress); - const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: constTunnel.localAddress }); - return { - resolved, - dispose: () => constTunnel.dispose(), - }; - } - } - } - - if (!options?.openExternal) { - const canHandleResource = await this.fileService.canHandleResource(uri); - if (canHandleResource) { - return { - resolved: URI.from({ - scheme: this.productService.urlProtocol, - path: 'workspace', - query: uri.toString() - }), - dispose() { } - }; - } - } - - return undefined; + return this.resolveExternalUri(uri, options); } }); } diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts new file mode 100644 index 00000000000..f5ab4a4a8fa --- /dev/null +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; +import { RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; +import { URI } from 'vs/base/common/uri'; + +type PortMap = Record; + +class TunnelMock { + private assignedPorts: PortMap = {}; + + reset(ports: PortMap) { + this.assignedPorts = ports; + } + + openTunnel(_address: string, port: number): Promise { + if (!this.assignedPorts[port]) { + return Promise.reject(new Error('Unexpected tunnel request')); + } + const res: RemoteTunnel = { + localAddress: `localhost:${this.assignedPorts[port]}`, + tunnelRemoteHost: '4.3.2.1', + tunnelRemotePort: this.assignedPorts[port], + privacy: '', + dispose: () => { + return Promise.resolve(); + } + }; + delete this.assignedPorts[port]; + return Promise.resolve(res); + } + + validate() { + assert(Object.keys(this.assignedPorts).length === 0, 'Expected tunnel to be used'); + } +} + +suite('NativeWindow:resolveExternal', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + const tunnelMock = new TunnelMock(); + + async function doTest(uri: string, ports: PortMap = {}, expectedUri?: string) { + tunnelMock.reset(ports); + const res = await NativeWindow.prototype.resolveExternalUri.call(tunnelMock, URI.parse(uri), { + allowTunneling: true, + openExternal: true + }); + assert.strictEqual(!expectedUri, !res, `Expected URI ${expectedUri} but got ${res}`); + if (expectedUri && res) { + assert.strictEqual(res.resolved.toString(), URI.parse(expectedUri).toString()); + } + tunnelMock.validate(); + } + test('invalid', async () => { + await doTest('file:///foo.bar/baz'); + await doTest('http://foo.bar/path'); + }); + test('simple', async () => { + await doTest('http://localhost:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); + }); + test('all interfaces', async () => { + await doTest('http://0.0.0.0:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); + }); + test('changed port', async () => { + await doTest('http://localhost:1234/path', { 1234: 1235 }, 'http://localhost:1235/path'); + }); +}); From 308c48ea0e1788f5edcf6c7a34ecad28e817b1c6 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Fri, 9 Feb 2024 10:44:55 +0200 Subject: [PATCH 043/141] Tunnel: Extend port mapping lookup also for querystring (take 2) When running az login, the URL has localhost in redirect_uri query param. This should trigger automatic port mapping. Improve localhost port mapping to cover this case as well. This is a revised and tested version of #203908 which was reverted in #205370. Fixes #203869 --- src/vs/platform/tunnel/common/tunnel.ts | 17 ++++++++++ .../tunnel/test/common/tunnel.test.ts | 17 ++++++++-- src/vs/workbench/electron-sandbox/window.ts | 30 +++++++++++++++-- .../electron-sandbox/resolveExternal.test.ts | 32 ++++++++++++++++++- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 62c9059d2f8..86b4da4b409 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -155,6 +155,23 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: }; } +export function extractQueryLocalHostUriMetaDataForPortMapping(uri: URI): { address: string; port: number } | undefined { + if (uri.scheme !== 'http' && uri.scheme !== 'https' || !uri.query) { + return undefined; + } + const keyvalues = uri.query.split('&'); + for (const keyvalue of keyvalues) { + const value = keyvalue.split('=')[1]; + if (/^https?:/.exec(value)) { + const result = extractLocalHostUriMetaDataForPortMapping(URI.parse(value)); + if (result) { + return result; + } + } + } + return undefined; +} + export const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1']; export function isLocalhost(host: string): boolean { return LOCALHOST_ADDRESSES.indexOf(host) >= 0; diff --git a/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts index 4884acb05d4..d86d3f47bd7 100644 --- a/src/vs/platform/tunnel/test/common/tunnel.test.ts +++ b/src/vs/platform/tunnel/test/common/tunnel.test.ts @@ -4,7 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; +import { + extractLocalHostUriMetaDataForPortMapping, + extractQueryLocalHostUriMetaDataForPortMapping +} from 'vs/platform/tunnel/common/tunnel'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -12,16 +15,21 @@ suite('Tunnel', () => { ensureNoDisposablesAreLeakedInTestSuite(); function portMappingDoTest(uri: string, + func: (uri: URI) => { address: string; port: number } | undefined, expectedAddress?: string, expectedPort?: number) { - const res = extractLocalHostUriMetaDataForPortMapping(URI.parse(uri)); + const res = func(URI.parse(uri)); assert.strictEqual(!expectedAddress, !res); assert.strictEqual(res?.address, expectedAddress); assert.strictEqual(res?.port, expectedPort); } function portMappingTest(uri: string, expectedAddress?: string, expectedPort?: number) { - portMappingDoTest(uri, expectedAddress, expectedPort); + portMappingDoTest(uri, extractLocalHostUriMetaDataForPortMapping, expectedAddress, expectedPort); + } + + function portMappingTestQuery(uri: string, expectedAddress?: string, expectedPort?: number) { + portMappingDoTest(uri, extractQueryLocalHostUriMetaDataForPortMapping, expectedAddress, expectedPort); } test('portMapping', () => { @@ -33,5 +41,8 @@ suite('Tunnel', () => { portMappingTest('http://0.0.0.0:7654', '0.0.0.0', 7654); portMappingTest('http://localhost:8080/path?foo=bar', 'localhost', 8080); portMappingTest('http://localhost:8080/path?foo=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8080); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8081); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Flocalhost%3A8081&url2=http%3A%2F%2Flocalhost%3A8082', 'localhost', 8081); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Fmicrosoft.com%2Fbad&url2=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8081); }); }); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 946e3a9ad5e..9f9877a34ff 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -46,7 +46,7 @@ import { IOpenerService, IResolvedExternalUri, OpenOptions } from 'vs/platform/o import { Schemas } from 'vs/base/common/network'; import { INativeHostService } from 'vs/platform/native/common/native'; import { posix } from 'vs/base/common/path'; -import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; +import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping, extractQueryLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; import { IWorkbenchLayoutService, Parts, positionFromString, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -799,8 +799,29 @@ export class NativeWindow extends BaseWindow { } async resolveExternalUri(uri: URI, options?: OpenOptions): Promise { + let queryTunnel: RemoteTunnel | string | undefined; if (options?.allowTunneling) { const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); + const queryPortMapping = extractQueryLocalHostUriMetaDataForPortMapping(uri); + if (queryPortMapping) { + queryTunnel = await this.openTunnel(queryPortMapping.address, queryPortMapping.port); + if (queryTunnel && (typeof queryTunnel !== 'string')) { + // If the tunnel was mapped to a different port, dispose it, because some services + // validate the port number in the query string. + if (queryTunnel.tunnelRemotePort !== queryPortMapping.port) { + queryTunnel.dispose(); + queryTunnel = undefined; + } else { + if (!portMappingRequest) { + const tunnel = queryTunnel; + return { + resolved: uri, + dispose: () => tunnel.dispose() + }; + } + } + } + } if (portMappingRequest) { const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port); if (tunnel && (typeof tunnel !== 'string')) { @@ -808,7 +829,12 @@ export class NativeWindow extends BaseWindow { const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: tunnel.localAddress }); return { resolved, - dispose: () => tunnel.dispose(), + dispose() { + tunnel.dispose(); + if (queryTunnel && (typeof queryTunnel !== 'string')) { + queryTunnel.dispose(); + } + } }; } } diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts index f5ab4a4a8fa..821367aea2f 100644 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -12,11 +12,16 @@ type PortMap = Record; class TunnelMock { private assignedPorts: PortMap = {}; + private expectedDispose = false; reset(ports: PortMap) { this.assignedPorts = ports; } + expectDispose() { + this.expectedDispose = true; + } + openTunnel(_address: string, port: number): Promise { if (!this.assignedPorts[port]) { return Promise.reject(new Error('Unexpected tunnel request')); @@ -27,6 +32,8 @@ class TunnelMock { tunnelRemotePort: this.assignedPorts[port], privacy: '', dispose: () => { + assert(this.expectedDispose, 'Unexpected dispose'); + this.expectedDispose = false; return Promise.resolve(); } }; @@ -35,7 +42,12 @@ class TunnelMock { } validate() { - assert(Object.keys(this.assignedPorts).length === 0, 'Expected tunnel to be used'); + try { + assert(Object.keys(this.assignedPorts).length === 0, 'Expected tunnel to be used'); + assert(!this.expectedDispose, 'Expected dispose to be called'); + } finally { + this.expectedDispose = false; + } } } @@ -68,4 +80,22 @@ suite('NativeWindow:resolveExternal', () => { test('changed port', async () => { await doTest('http://localhost:1234/path', { 1234: 1235 }, 'http://localhost:1235/path'); }); + test('query', async () => { + await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4455 }, 'http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); + test('query with different port', async () => { + tunnelMock.expectDispose(); + await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4567 }); + }); + test('both url and query', async () => { + await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', + { 1234: 4321, 4455: 4455 }, + 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); + test('both url and query, query rejected', async () => { + tunnelMock.expectDispose(); + await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', + { 1234: 4321, 4455: 5544 }, + 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); }); From aaaa801e8a5befc744516103ba225a947668d21e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 7 Mar 2024 17:19:06 +0100 Subject: [PATCH 044/141] Joh/husky-aardvark (#207088) * chore - pull-out input editor part of the inline chat widget * cleanup --- .../contrib/inlineChat/browser/inlineChat.css | 9 +- .../browser/inlineChatInputWidget.ts | 394 ++++++++++++++++++ .../inlineChat/browser/inlineChatWidget.ts | 385 +++-------------- 3 files changed, 444 insertions(+), 344 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 70ff20e6432..1b5ea26167f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -14,7 +14,6 @@ .monaco-workbench .inline-chat { color: inherit; padding: 6px; - margin-top: 6px; border-radius: 6px; border: 1px solid var(--vscode-inlineChat-border); box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); @@ -105,16 +104,12 @@ /* status */ .monaco-workbench .inline-chat .status { - margin-top: 4px; + padding-top: 4px; display: flex; justify-content: space-between; align-items: center; } -.monaco-workbench .inline-chat .status.actions { - margin-top: 4px; -} - .monaco-workbench .inline-chat .status .actions.hidden { display: none; } @@ -198,7 +193,7 @@ } .monaco-workbench .inline-chat .chatMessage { - padding: 8px 3px; + padding: 0 3px; } .monaco-workbench .inline-chat .chatMessage .chatMessageContent { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts new file mode 100644 index 00000000000..f32ce39084e --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts @@ -0,0 +1,394 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { Dimension, addDisposableListener, getTotalWidth, h } from 'vs/base/browser/dom'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; +import { Range } from 'vs/editor/common/core/range'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { Position } from 'vs/editor/common/core/position'; +import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { localize } from 'vs/nls'; + + +export class InlineChatInputWidget { + + private readonly _elements = h( + 'div.content@content', + [ + h('div.input@input', [ + h('div.editor-placeholder@placeholder'), + h('div.editor-container@editor'), + ]), + h('div.toolbar@editorToolbar') + ] + ); + + private readonly _store = new DisposableStore(); + + private readonly _ctxInputEmpty: IContextKey; + private readonly _ctxInnerCursorFirst: IContextKey; + private readonly _ctxInnerCursorLast: IContextKey; + private readonly _ctxInnerCursorStart: IContextKey; + private readonly _ctxInnerCursorEnd: IContextKey; + private readonly _ctxInputEditorFocused: IContextKey; + // private readonly _ctxResponseFocused: IContextKey; + + private readonly _inputEditor: IActiveCodeEditor; + private readonly _inputModel: ITextModel; + + private readonly _slashCommandContentWidget: SlashCommandContentWidget; + private readonly _slashCommands = this._store.add(new DisposableStore()); + private _slashCommandDetails: { command: string; detail: string }[] = []; + + protected readonly _onDidChangeHeight = this._store.add(new Emitter()); + readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; + + private readonly _onDidChangeInput = this._store.add(new Emitter()); + readonly onDidChangeInput: Event = this._onDidChangeInput.event; + + constructor( + options: { menuId: MenuId; telemetrySource: string; hoverDelegate: IHoverDelegate }, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + ) { + + this._inputEditor = instantiationService.createInstance(CodeEditorWidget, this._elements.editor, inputEditorOptions, codeEditorWidgetOptions); + this._store.add(this._inputEditor); + this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); + this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); + this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); + + const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${generateUuid()}.txt` }); + this._inputModel = this._store.add(modelService.getModel(uri) ?? modelService.createModel('', null, uri)); + this._inputEditor.setModel(this._inputModel); + + // placeholder + this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; + this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; + this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); + + // slash command content widget + this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); + this._store.add(this._slashCommandContentWidget); + + // toolbar + this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.menuId, { + telemetrySource: options.telemetrySource, + toolbarOptions: { primaryGroup: 'main' }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu + hoverDelegate: options.hoverDelegate + })); + + + this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(contextKeyService); + this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(contextKeyService); + this._ctxInnerCursorLast = CTX_INLINE_CHAT_INNER_CURSOR_LAST.bindTo(contextKeyService); + this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(contextKeyService); + this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(contextKeyService); + this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(contextKeyService); + + // (1) inner cursor position (last/first line selected) + const updateInnerCursorFirstLast = () => { + const selection = this._inputEditor.getSelection(); + const fullRange = this._inputModel.getFullModelRange(); + let onFirst = false; + let onLast = false; + if (selection.isEmpty()) { + const selectionTop = this._inputEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); + const firstViewLineTop = this._inputEditor.getTopForPosition(fullRange.startLineNumber, fullRange.startColumn); + const lastViewLineTop = this._inputEditor.getTopForPosition(fullRange.endLineNumber, fullRange.endColumn); + + if (selectionTop === firstViewLineTop) { + onFirst = true; + } + if (selectionTop === lastViewLineTop) { + onLast = true; + } + } + this._ctxInnerCursorFirst.set(onFirst); + this._ctxInnerCursorLast.set(onLast); + this._ctxInnerCursorStart.set(fullRange.getStartPosition().equals(selection.getStartPosition())); + this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); + }; + this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); + updateInnerCursorFirstLast(); + + // (2) input editor focused or not + const updateFocused = () => { + const hasFocus = this._inputEditor.hasWidgetFocus(); + this._ctxInputEditorFocused.set(hasFocus); + this._elements.content.classList.toggle('synthetic-focus', hasFocus); + this.readPlaceholder(); + }; + this._store.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); + this._store.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); + this._store.add(toDisposable(() => { + this._ctxInnerCursorFirst.reset(); + this._ctxInnerCursorLast.reset(); + this._ctxInputEditorFocused.reset(); + })); + updateFocused(); + + + // show/hide placeholder depending on text model being empty + // content height + const currentContentHeight = 0; + const togglePlaceholder = () => { + const hasText = this._inputModel.getValueLength() > 0; + this._elements.placeholder.classList.toggle('hidden', hasText); + this._ctxInputEmpty.set(!hasText); + this.readPlaceholder(); + + const contentHeight = this._inputEditor.getContentHeight(); + if (contentHeight !== currentContentHeight) { + this._onDidChangeHeight.fire(); + } + }; + this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); + togglePlaceholder(); + } + + dispose(): void { + this.reset(); + this._store.dispose(); + } + + get domNode() { + return this._elements.content; + } + + layout(dim: Dimension) { + const toolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; + const editorWidth = dim.width - toolbarWidth; + this._inputEditor.layout({ height: dim.height, width: editorWidth }); + this._elements.placeholder.style.width = `${editorWidth}px`; + } + + getPreferredHeight(): number { + return this._inputEditor.getContentHeight(); + } + + reset() { + this._ctxInputEmpty.reset(); + this._ctxInnerCursorFirst.reset(); + this._ctxInnerCursorLast.reset(); + this._ctxInnerCursorStart.reset(); + this._ctxInnerCursorEnd.reset(); + this._ctxInputEditorFocused.reset(); + this.value = ''; + } + + focus() { + this._inputEditor.focus(); + } + + get value(): string { + return this._inputModel.getValue(); + } + + set value(value: string) { + this._inputModel.setValue(value); + this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); + } + + selectAll(includeSlashCommand: boolean = true) { + let selection = this._inputModel.getFullModelRange(); + + if (!includeSlashCommand) { + const firstLine = this._inputModel.getLineContent(1); + const slashCommand = this._slashCommandDetails.find(c => firstLine.startsWith(`/${c.command} `)); + selection = slashCommand ? new Range(1, slashCommand.command.length + 3, selection.endLineNumber, selection.endColumn) : selection; + } + + this._inputEditor.setSelection(selection); + } + + set ariaLabel(label: string) { + this._inputEditor.updateOptions({ ariaLabel: label }); + } + + set placeholder(value: string) { + this._elements.placeholder.innerText = value; + } + + readPlaceholder(): void { + const slashCommand = this._slashCommandDetails.find(c => `${c.command} ` === this._inputModel.getValue().substring(1)); + const hasText = this._inputModel.getValueLength() > 0; + if (!hasText) { + aria.status(this._elements.placeholder.innerText); + } else if (slashCommand) { + aria.status(slashCommand.detail); + } + } + + updateSlashCommands(commands: IInlineChatSlashCommand[]) { + + this._slashCommands.clear(); + this._slashCommandDetails = commands.filter(c => c.command && c.detail).map(c => { return { command: c.command, detail: c.detail! }; }); + + if (this._slashCommandDetails.length === 0) { + return; + } + + const selector: LanguageSelector = { scheme: this._inputModel.uri.scheme, pattern: this._inputModel.uri.path, language: this._inputModel.getLanguageId() }; + this._slashCommands.add(this._languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { + + _debugDisplayName: string = 'InlineChatSlashCommandProvider'; + + readonly triggerCharacters?: string[] = ['/']; + + provideCompletionItems(_model: ITextModel, position: Position): ProviderResult { + if (position.lineNumber !== 1 && position.column !== 1) { + return undefined; + } + + const suggestions: CompletionItem[] = commands.map(command => { + + const withSlash = `/${command.command}`; + + return { + label: { label: withSlash, description: command.detail }, + insertText: `${withSlash} $0`, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + kind: CompletionItemKind.Text, + range: new Range(1, 1, 1, 1), + command: command.executeImmediately ? { id: 'inlineChat.accept', title: withSlash } : undefined + }; + }); + + return { suggestions }; + } + })); + + const decorations = this._inputEditor.createDecorationsCollection(); + + const updateSlashDecorations = () => { + this._slashCommandContentWidget.hide(); + // TODO@jrieken + // this._elements.detectedIntent.classList.toggle('hidden', true); + + const newDecorations: IModelDeltaDecoration[] = []; + for (const command of commands) { + const withSlash = `/${command.command}`; + const firstLine = this._inputModel.getLineContent(1); + if (firstLine.startsWith(withSlash)) { + newDecorations.push({ + range: new Range(1, 1, 1, withSlash.length + 1), + options: { + description: 'inline-chat-slash-command', + inlineClassName: 'inline-chat-slash-command', + after: { + // Force some space between slash command and placeholder + content: ' ' + } + } + }); + + this._slashCommandContentWidget.setCommandText(command.command); + this._slashCommandContentWidget.show(); + + // inject detail when otherwise empty + if (firstLine === `/${command.command}`) { + newDecorations.push({ + range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), + options: { + description: 'inline-chat-slash-command-detail', + after: { + content: `${command.detail}`, + inlineClassName: 'inline-chat-slash-command-detail' + } + } + }); + } + break; + } + } + decorations.set(newDecorations); + }; + + this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); + updateSlashDecorations(); + } +} + +export const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); + +export const inputEditorOptions: IEditorConstructionOptions = { + padding: { top: 2, bottom: 2 }, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + hideCursorInOverviewRuler: true, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + useShadows: false, + vertical: 'hidden', + horizontal: 'auto', + alwaysConsumeMouseWheel: false + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + dragAndDrop: false, + revealHorizontalRightPadding: 5, + minimap: { enabled: false }, + guides: { indentation: false }, + rulers: [], + cursorWidth: 1, + cursorStyle: 'line', + cursorBlinking: 'blink', + wrappingStrategy: 'advanced', + wrappingIndent: 'none', + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + quickSuggestions: false, + suggest: { + showIcons: false, + showSnippets: false, + showWords: true, + showStatusBar: false, + }, + wordWrap: 'on', + ariaLabel: defaultAriaLabel, + fontFamily: DEFAULT_FONT_FAMILY, + fontSize: 13, + lineHeight: 20 +}; + +export const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SnippetController2.ID, + SuggestController.ID + ]) +}; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 25d8dbb6cfa..fb7f2c67237 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -5,20 +5,15 @@ import { Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import * as aria from 'vs/base/browser/ui/aria/aria'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; -import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; -import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; @@ -28,14 +23,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { LanguageSelector } from 'vs/editor/common/languageSelector'; -import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; -import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModel } from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; @@ -49,80 +38,24 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { inputEditorOptions, codeEditorWidgetOptions, InlineChatInputWidget, defaultAriaLabel } from './inlineChatInputWidget'; -const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); - -export const _inputEditorOptions: IEditorConstructionOptions = { - padding: { top: 2, bottom: 2 }, - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - hideCursorInOverviewRuler: true, - selectOnLineNumbers: false, - selectionHighlight: false, - scrollbar: { - useShadows: false, - vertical: 'hidden', - horizontal: 'auto', - alwaysConsumeMouseWheel: false - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - dragAndDrop: false, - revealHorizontalRightPadding: 5, - minimap: { enabled: false }, - guides: { indentation: false }, - rulers: [], - cursorWidth: 1, - cursorStyle: 'line', - cursorBlinking: 'blink', - wrappingStrategy: 'advanced', - wrappingIndent: 'none', - renderWhitespace: 'none', - dropIntoEditor: { enabled: true }, - quickSuggestions: false, - suggest: { - showIcons: false, - showSnippets: false, - showWords: true, - showStatusBar: false, - }, - wordWrap: 'on', - ariaLabel: defaultAriaLabel, - fontFamily: DEFAULT_FONT_FAMILY, - fontSize: 13, - lineHeight: 20 -}; - -const _codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - SnippetController2.ID, - SuggestController.ID - ]) -}; const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, @@ -180,25 +113,17 @@ export interface IInlineChatMessageAppender { export class InlineChatWidget { - private static _modelPool: number = 1; - protected readonly _elements = h( 'div.inline-chat@root', [ - h('div.body', [ - h('div.content@content', [ - h('div.input@input', [ - h('div.editor-placeholder@placeholder'), - h('div.editor-container@editor'), - ]), - h('div.toolbar@editorToolbar'), - ]), + h('div.body@body', [ + h('div.content@content'), h('div.widget-toolbar@widgetToolbar') ]), h('div.progress@progress'), h('div.detectedIntent.hidden@detectedIntent'), h('div.previewDiff.hidden@previewDiff'), - h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), + h('div.previewCreateTitle.show-file-icons.hidden@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), h('div.chatMessage.hidden@chatMessage'), h('div.followUps.hidden@followUps'), @@ -216,16 +141,9 @@ export class InlineChatWidget { private readonly _chatMessageScrollable: DomScrollableElement; protected readonly _store = new DisposableStore(); - private readonly _slashCommands = this._store.add(new DisposableStore()); - private readonly _inputEditor: IActiveCodeEditor; - private readonly _inputModel: ITextModel; - private readonly _ctxInputEmpty: IContextKey; - private readonly _ctxInnerCursorFirst: IContextKey; - private readonly _ctxInnerCursorLast: IContextKey; - private readonly _ctxInnerCursorStart: IContextKey; - private readonly _ctxInnerCursorEnd: IContextKey; - private readonly _ctxInputEditorFocused: IContextKey; + private readonly _inputWidget: InlineChatInputWidget; + private readonly _ctxResponseFocused: IContextKey; private readonly _progressBar: ProgressBar; @@ -243,9 +161,7 @@ export class InlineChatWidget { private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - private _slashCommandDetails: { command: string; detail: string }[] = []; - private _slashCommandContentWidget: SlashCommandContentWidget; private readonly _editorOptions: ChatEditorOptions; private _chatMessageDisposables = this._store.add(new DisposableStore()); @@ -258,9 +174,7 @@ export class InlineChatWidget { constructor( options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -269,20 +183,16 @@ export class InlineChatWidget { @ITextModelService protected readonly _textModelResolverService: ITextModelService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { + // Share hover delegates between toolbars to support instant hover between both + const hoverDelegate = this._store.add(createInstantHoverDelegate()); // input editor logic - this._inputEditor = this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, _inputEditorOptions, _codeEditorWidgetOptions); - this._updateAriaLabel(); - this._store.add(this._inputEditor); - this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); - this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); - this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); + this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: options.inputMenuId, telemetrySource: options.telemetrySource, hoverDelegate }); + this._elements.body.replaceChild(this._inputWidget.domNode, this._elements.content); + this._store.add(this._inputWidget); + this._store.add(this._inputWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); - this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); - this._inputEditor.setModel(this._inputModel); - this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground)); this._chatMessageContents = document.createElement('div'); @@ -301,103 +211,10 @@ export class InlineChatWidget { } })); - // --- context keys - - this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); - - this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); - this._ctxInnerCursorLast = CTX_INLINE_CHAT_INNER_CURSOR_LAST.bindTo(this._contextKeyService); - this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(this._contextKeyService); - this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(this._contextKeyService); - this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this._contextKeyService); + // context keys this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); - // (1) inner cursor position (last/first line selected) - const updateInnerCursorFirstLast = () => { - const selection = this._inputEditor.getSelection(); - const fullRange = this._inputModel.getFullModelRange(); - let onFirst = false; - let onLast = false; - if (selection.isEmpty()) { - const selectionTop = this._inputEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); - const firstViewLineTop = this._inputEditor.getTopForPosition(fullRange.startLineNumber, fullRange.startColumn); - const lastViewLineTop = this._inputEditor.getTopForPosition(fullRange.endLineNumber, fullRange.endColumn); - - if (selectionTop === firstViewLineTop) { - onFirst = true; - } - if (selectionTop === lastViewLineTop) { - onLast = true; - } - } - this._ctxInnerCursorFirst.set(onFirst); - this._ctxInnerCursorLast.set(onLast); - this._ctxInnerCursorStart.set(fullRange.getStartPosition().equals(selection.getStartPosition())); - this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); - }; - this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); - updateInnerCursorFirstLast(); - - // (2) input editor focused or not - const updateFocused = () => { - const hasFocus = this._inputEditor.hasWidgetFocus(); - this._ctxInputEditorFocused.set(hasFocus); - this._elements.content.classList.toggle('synthetic-focus', hasFocus); - this.readPlaceholder(); - }; - this._store.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); - this._store.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); - this._store.add(toDisposable(() => { - this._ctxInnerCursorFirst.reset(); - this._ctxInnerCursorLast.reset(); - this._ctxInputEditorFocused.reset(); - })); - updateFocused(); - - // placeholder - - this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; - this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; - this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); - - // show/hide placeholder depending on text model being empty - // content height - - const currentContentHeight = 0; - - const togglePlaceholder = () => { - const hasText = this._inputModel.getValueLength() > 0; - this._elements.placeholder.classList.toggle('hidden', hasText); - this._ctxInputEmpty.set(!hasText); - this.readPlaceholder(); - - const contentHeight = this._inputEditor.getContentHeight(); - if (contentHeight !== currentContentHeight && this._lastDim) { - this._lastDim = this._lastDim.with(undefined, contentHeight); - this._inputEditor.layout(this._lastDim); - this._onDidChangeHeight.fire(); - } - }; - this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); - togglePlaceholder(); - - // slash command content widget - - this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); - this._store.add(this._slashCommandContentWidget); - - // Share hover delegates between toolbars to support instant hover between both - const hoverDelegate = this._store.add(createInstantHoverDelegate()); - // toolbars - - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.inputMenuId, { - telemetrySource: options.telemetrySource, - toolbarOptions: { primaryGroup: 'main' }, - hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu - hoverDelegate - })); - this._progressBar = new ProgressBar(this._elements.progress); this._store.add(this._progressBar); @@ -446,7 +263,6 @@ export class InlineChatWidget { this._codeBlockModelCollection = this._store.add(this._instantiationService.createInstance(CodeBlockModelCollection)); } - private _updateAriaLabel(): void { if (!this._accessibilityService.isScreenReaderOptimized()) { return; @@ -456,13 +272,12 @@ export class InlineChatWidget { const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); label = kbLabel ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - _inputEditorOptions.ariaLabel = label; - this._inputEditor.updateOptions({ ariaLabel: label }); + inputEditorOptions.ariaLabel = label; + this._inputWidget.ariaLabel = label; } dispose(): void { this._store.dispose(); - this._ctxInputEmpty.reset(); } get domNode(): HTMLElement { @@ -473,16 +288,14 @@ export class InlineChatWidget { this._isLayouting = true; try { const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); - const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; - const innerEditorWidth = widgetDim.width - editorToolbarWidth - widgetToolbarWidth; - const inputDim = new Dimension(innerEditorWidth, widgetDim.height); + const innerEditorWidth = widgetDim.width - widgetToolbarWidth; + const inputDim = new Dimension(innerEditorWidth, this._inputWidget.getPreferredHeight()); if (!this._lastDim || !Dimension.equals(this._lastDim, inputDim)) { this._lastDim = inputDim; this._doLayout(widgetDim, inputDim); - - this._onDidChangeLayout.fire(); } } finally { + this._onDidChangeLayout.fire(); this._isLayouting = false; } } @@ -490,19 +303,18 @@ export class InlineChatWidget { protected _doLayout(widgetDimension: Dimension, inputDimension: Dimension): void { this._chatMessageContents.style.width = `${widgetDimension.width - 10}px`; this._chatMessageContents.style.maxHeight = `270px`; - this._inputEditor.layout(new Dimension(inputDimension.width, this._inputEditor.getContentHeight())); - this._elements.placeholder.style.width = `${inputDimension.width}px`; + + this._inputWidget.layout(inputDimension); } getHeight(): number { - const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; + const editorHeight = this._inputWidget.getPreferredHeight() + 4 /*padding*/; const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); - const chatResponseHeight = getTotalHeight(this._chatMessageContents) + 16 /*padding*/; + const chatResponseHeight = getTotalHeight(this._chatMessageContents); const followUpsHeight = getTotalHeight(this._elements.followUps); - const statusHeight = getTotalHeight(this._elements.status); - return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + statusHeight + 18 /* padding */ + 8 /*shadow*/; + return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + statusHeight + 12 /* padding */ + 2 /*border*/ + 12 /*shadow*/; } updateProgress(show: boolean) { @@ -516,38 +328,23 @@ export class InlineChatWidget { } get value(): string { - return this._inputModel.getValue(); + return this._inputWidget.value; } set value(value: string) { - this._inputModel.setValue(value); - this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); + this._inputWidget.value = value; } selectAll(includeSlashCommand: boolean = true) { - let selection = this._inputModel.getFullModelRange(); - - if (!includeSlashCommand) { - const firstLine = this._inputModel.getLineContent(1); - const slashCommand = this._slashCommandDetails.find(c => firstLine.startsWith(`/${c.command} `)); - selection = slashCommand ? new Range(1, slashCommand.command.length + 3, selection.endLineNumber, selection.endColumn) : selection; - } - - this._inputEditor.setSelection(selection); + this._inputWidget.selectAll(includeSlashCommand); } set placeholder(value: string) { - this._elements.placeholder.innerText = value; + this._inputWidget.placeholder = value; } readPlaceholder(): void { - const slashCommand = this._slashCommandDetails.find(c => `${c.command} ` === this._inputModel.getValue().substring(1)); - const hasText = this._inputModel.getValueLength() > 0; - if (!hasText) { - aria.status(this._elements.placeholder.innerText); - } else if (slashCommand) { - aria.status(slashCommand.detail); - } + this._inputWidget.readPlaceholder(); } updateToolbar(show: boolean) { @@ -596,6 +393,9 @@ export class InlineChatWidget { appendContent: (fragment: string) => { responseModel.updateContent({ kind: 'markdownContent', content: new MarkdownString(fragment) }); this._chatMessage?.appendMarkdown(fragment); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); + this._onDidChangeHeight.fire(); } } : undefined; } @@ -616,8 +416,15 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } + private _currentSlashCommands: IInlineChatSlashCommand[] = []; + + updateSlashCommands(commands: IInlineChatSlashCommand[]) { + this._currentSlashCommands = commands; + this._inputWidget.updateSlashCommands(commands); + } + updateSlashCommandUsed(command: string): void { - const details = this._slashCommandDetails.find(candidate => candidate.command === command); + const details = this._currentSlashCommands.find(candidate => candidate.command === command); if (!details) { return; } @@ -675,12 +482,7 @@ export class InlineChatWidget { } reset() { - this._ctxInputEmpty.reset(); - this._ctxInnerCursorFirst.reset(); - this._ctxInnerCursorLast.reset(); - this._ctxInputEditorFocused.reset(); - - this.value = ''; + this._inputWidget.reset(); this.updateChatMessage(undefined); this.updateFollowUps(undefined); @@ -697,102 +499,13 @@ export class InlineChatWidget { } focus() { - this._inputEditor.focus(); + this._inputWidget.focus(); } hasFocus() { return this.domNode.contains(getActiveElement()); } - // --- slash commands - - updateSlashCommands(commands: IInlineChatSlashCommand[]) { - - this._slashCommands.clear(); - - if (commands.length === 0) { - return; - } - this._slashCommandDetails = commands.filter(c => c.command && c.detail).map(c => { return { command: c.command, detail: c.detail! }; }); - - const selector: LanguageSelector = { scheme: this._inputModel.uri.scheme, pattern: this._inputModel.uri.path, language: this._inputModel.getLanguageId() }; - this._slashCommands.add(this._languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { - - _debugDisplayName: string = 'InlineChatSlashCommandProvider'; - - readonly triggerCharacters?: string[] = ['/']; - - provideCompletionItems(_model: ITextModel, position: Position): ProviderResult { - if (position.lineNumber !== 1 && position.column !== 1) { - return undefined; - } - - const suggestions: CompletionItem[] = commands.map(command => { - - const withSlash = `/${command.command}`; - - return { - label: { label: withSlash, description: command.detail }, - insertText: `${withSlash} $0`, - insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, - kind: CompletionItemKind.Text, - range: new Range(1, 1, 1, 1), - command: command.executeImmediately ? { id: 'inlineChat.accept', title: withSlash } : undefined - }; - }); - - return { suggestions }; - } - })); - - const decorations = this._inputEditor.createDecorationsCollection(); - - const updateSlashDecorations = () => { - this._slashCommandContentWidget.hide(); - this._elements.detectedIntent.classList.toggle('hidden', true); - - const newDecorations: IModelDeltaDecoration[] = []; - for (const command of commands) { - const withSlash = `/${command.command}`; - const firstLine = this._inputModel.getLineContent(1); - if (firstLine.startsWith(withSlash)) { - newDecorations.push({ - range: new Range(1, 1, 1, withSlash.length + 1), - options: { - description: 'inline-chat-slash-command', - inlineClassName: 'inline-chat-slash-command', - after: { - // Force some space between slash command and placeholder - content: ' ' - } - } - }); - - this._slashCommandContentWidget.setCommandText(command.command); - this._slashCommandContentWidget.show(); - - // inject detail when otherwise empty - if (firstLine === `/${command.command}`) { - newDecorations.push({ - range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), - options: { - description: 'inline-chat-slash-command-detail', - after: { - content: `${command.detail}`, - inlineClassName: 'inline-chat-slash-command-detail' - } - } - }); - } - break; - } - } - decorations.set(newDecorations); - }; - - this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); - updateSlashDecorations(); - } } export class EditorBasedInlineChatWidget extends InlineChatWidget { @@ -809,9 +522,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { constructor( private readonly _parentEditor: ICodeEditor, options: IInlineChatWidgetConstructionOptions, - @IModelService modelService: IModelService, @IContextKeyService contextKeyService: IContextKeyService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService accessibilityService: IAccessibilityService, @@ -821,17 +532,17 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @ITextModelService textModelResolverService: ITextModelService, @IChatAgentService chatAgentService: IChatAgentService, ) { - super(options, instantiationService, modelService, contextKeyService, languageFeaturesService, keybindingService, accessibilityService, configurationService, accessibleViewService, logService, textModelResolverService, chatAgentService,); + super(options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, logService, textModelResolverService, chatAgentService,); // preview editors this._previewDiffEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, { useInlineViewWhenSpaceIsLimited: false, ..._previewEditorEditorOptions, onlyShowAccessibleDiffViewer: accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: _codeEditorWidgetOptions, originalEditor: _codeEditorWidgetOptions }, _parentEditor))); + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor))); this._previewCreateTitle = this._store.add(instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, _codeEditorWidgetOptions, _parentEditor))); + this._previewCreateEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor))); } // --- layout From 5c90480e9b67fe2682d489cdb887f1a80cc8b937 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:50:48 +0100 Subject: [PATCH 045/141] Add border bottom to activity bar top (#207094) add border bottom when activity bar top --- src/vs/workbench/browser/parts/media/paneCompositePart.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 2155798ed82..455b38249d9 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,6 +20,10 @@ display: none; } +.monaco-workbench .pane-composite-part > .header-or-footer.header { + border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border); +} + .monaco-workbench .pane-composite-part > .header-or-footer.footer { border-top: 1px solid var(--vscode-sideBarSectionHeader-border); } From 46ff95b5b10f62743a78174a64b5c4b16ee19707 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 7 Mar 2024 10:01:56 -0800 Subject: [PATCH 046/141] Pick up latest stable TS release (#207095) --- extensions/package.json | 2 +- extensions/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/package.json b/extensions/package.json index 7066412c3f8..ab93f194b51 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.4.1-rc" + "typescript": "5.4" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 4b22ef50a82..23ab31c1254 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -234,10 +234,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@5.4.1-rc: - version "5.4.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.1-rc.tgz#1ecdd897df1d9ef5bd1f844bad64691ecc23314d" - integrity sha512-gInURzaO0bbfzfQAc3mfcHxh8qev+No4QOFUZHajo9vBgOLaljELJ3wuzyoGo/zHIzMSezdhtrsRdqL6E9SvNA== +typescript@5.4: + version "5.4.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" + integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== vscode-grammar-updater@^1.1.0: version "1.1.0" From 3c318f2facf9a8fd28617b6a8d52b7acf5c45c4d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 11:31:23 -0800 Subject: [PATCH 047/141] wip move inline chat response inside of widget --- .../chat/browser/media/terminalChatWidget.css | 23 --- .../chat/browser/terminalChatController.ts | 29 +-- .../chat/browser/terminalChatWidget.ts | 179 +----------------- 3 files changed, 8 insertions(+), 223 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 64253f67db5..ad8623e881c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -19,29 +19,6 @@ visibility: hidden; } -.terminal-inline-chat-response.hide { - visibility: hidden; -} - .terminal-inline-chat .chatMessageContent { width: 400px !important; } - -.terminal-inline-chat .terminal-inline-chat-response { - border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-panel-background); -} - -.terminal-inline-chat .terminal-inline-chat-response:has(.monaco-editor.focused) { - border-color: var(--vscode-focusBorder, transparent); -} - -.terminal-inline-chat .terminal-inline-chat-response, -.terminal-inline-chat .terminal-inline-chat-response .monaco-editor, -.terminal-inline-chat .terminal-inline-chat-response .monaco-editor .overflow-guard { - border-radius: 4px; -} - -.terminal-inline-chat .terminal-inline-chat-response { - padding-left: 8px; -} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 2a505e3a1d3..36988724bbb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -8,7 +8,6 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; -import { marked } from 'vs/base/common/marked/marked'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -22,10 +21,9 @@ import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTermi import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; - - import { ChatModel, ChatRequestModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MarkdownString } from 'vs/base/common/htmlContent'; const enum Message { NONE = 0, @@ -236,8 +234,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey.set(true); const cancellationToken = new CancellationTokenSource().token; let responseContent = ''; - let firstCodeBlock: string | undefined; - let shellType: string | undefined; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { return; @@ -249,22 +245,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._model?.acceptResponseProgress(this._currentRequest, progress); } - if (!firstCodeBlock) { - const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; - if (firstCodeBlockContent) { - const regex = /```(?[\w\n]+)\n(?[\s\S]*?)```/g; - const match = regex.exec(firstCodeBlockContent); - firstCodeBlock = match?.groups?.content.trim(); - shellType = match?.groups?.language; - if (firstCodeBlock) { - this._chatWidget?.value.renderTerminalCommand(firstCodeBlock, shellType); - this._chatAccessibilityService.acceptResponse(firstCodeBlock, accessibilityRequestId); - this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); - this._messages.fire(Message.ACCEPT_INPUT); - } - } - } }; await this._model?.waitForInitialization(); @@ -301,10 +281,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._model?.completeResponse(this._currentRequest); } this._lastResponseContent = responseContent; - if (!firstCodeBlock && this._currentRequest) { + if (this._currentRequest) { this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); - this._chatWidget?.value.renderMessage(responseContent, this._currentRequest.id); - this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); + this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id, providerId: 'terminal' }); + const containsCode = responseContent.includes('```'); + this._responseTypeContextKey.set(containsCode ? TerminalChatResponseTypes.TerminalCommand : TerminalChatResponseTypes.Message); this._chatWidget?.value.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 85523a67b57..dcc6e9d5d55 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,21 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dimension, IFocusTracker, hide, show, trackFocus } from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/terminalChatWidget'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -30,8 +21,6 @@ export class TerminalChatWidget extends Disposable { private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } - private _responseEditor: TerminalChatResponseEditor | undefined; - private readonly _focusTracker: IFocusTracker; private readonly _focusedContextKey: IContextKey; @@ -73,19 +62,6 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); } - async renderTerminalCommand(command: string, shellType?: string): Promise { - this._responseEditor?.show(); - if (!this._responseEditor) { - this._responseEditor = this._instantiationService.createInstance(TerminalChatResponseEditor, command, shellType, this._container, this._instance); - } - this._responseEditor.setValue(command); - } - - renderMessage(message: string, requestId: string): void { - this._responseEditor?.hide(); - this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); - } - reveal(): void { this._inlineChatWidget.layout(new Dimension(640, 150)); this._container.classList.remove('hide'); @@ -118,8 +94,6 @@ export class TerminalChatWidget extends Disposable { hide(): void { this._container.classList.add('hide'); this._reset(); - this._responseEditor?.dispose(); - this._responseEditor = undefined; this._inlineChatWidget.updateChatMessage(undefined); this._inlineChatWidget.updateFollowUps(undefined); this._inlineChatWidget.updateProgress(false); @@ -140,13 +114,10 @@ export class TerminalChatWidget extends Disposable { } setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; - if (!value) { - this._responseEditor?.hide(); - } } acceptCommand(shouldExecute: boolean): void { // Trim command to remove any whitespace, otherwise this may execute the command - const value = this._responseEditor?.getValue().trim(); + const value = this._inlineChatWidget?.responseContent?.trim(); if (!value) { return; } @@ -161,147 +132,3 @@ export class TerminalChatWidget extends Disposable { } } -class TerminalChatResponseEditor extends Disposable { - private readonly _editorContainer: HTMLElement; - private readonly _editor: CodeEditorWidget; - - private readonly _responseEditorFocusedContextKey: IContextKey; - - readonly model: Promise; - - constructor( - initialCommandResponse: string, - shellType: string | undefined, - private readonly _container: HTMLElement, - private readonly _instance: ITerminalInstance, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILanguageService private readonly _languageService: ILanguageService, - @IModelService private readonly _modelService: IModelService, - ) { - super(); - - this._responseEditorFocusedContextKey = TerminalChatContextKeys.responseEditorFocused.bindTo(this._contextKeyService); - - this._editorContainer = document.createElement('div'); - this._editorContainer.classList.add('terminal-inline-chat-response'); - this._container.prepend(this._editorContainer); - this._register(toDisposable(() => this._editorContainer.remove())); - const editor = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, { - readOnly: false, - ariaLabel: this._getAriaLabel(), - fontSize: 13, - lineHeight: 20, - padding: { top: 8, bottom: 8 }, - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - hideCursorInOverviewRuler: true, - selectOnLineNumbers: false, - selectionHighlight: false, - scrollbar: { - useShadows: false, - vertical: 'hidden', - horizontal: 'hidden', - alwaysConsumeMouseWheel: false - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - dragAndDrop: false, - revealHorizontalRightPadding: 5, - minimap: { enabled: false }, - guides: { indentation: false }, - rulers: [], - renderWhitespace: 'none', - dropIntoEditor: { enabled: true }, - quickSuggestions: false, - suggest: { - showIcons: false, - showSnippets: false, - showWords: true, - showStatusBar: false, - }, - wordWrap: 'on' - }, { isSimpleWidget: true })); - this._editor = editor; - this._register(editor.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); - this._register(editor.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); - this._register(Event.any(editor.onDidChangeModelContent, editor.onDidChangeModelDecorations)(() => { - const height = editor.getContentHeight(); - editor.layout(new Dimension(640, height)); - })); - - this.model = this._getTextModel(URI.from({ - path: `terminal-inline-chat-${this._instance.instanceId}`, - scheme: 'terminal-inline-chat', - fragment: initialCommandResponse - })); - this.model.then(model => { - if (model) { - // Initial layout - editor.layout(new Dimension(640, 0)); - editor.setModel(model); - const height = editor.getContentHeight(); - editor.layout(new Dimension(640, height)); - - // Initialize language - const languageId = this._getLanguageFromShell(shellType); - model.setLanguage(languageId); - } - }); - } - - private _getAriaLabel(): string { - const verbose = this._configurationService.getValue(AccessibilityVerbositySettingId.Chat); - if (verbose) { - // TODO: Add verbose description - } - return localize('terminalResponseEditor', "Terminal Response Editor"); - } - - private async _getTextModel(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing && !existing.isDisposed()) { - return existing; - } - return this._modelService.createModel(resource.fragment, null, resource, false); - } - - private _getLanguageFromShell(shell?: string): string { - switch (shell) { - case 'fish': - return this._languageService.isRegisteredLanguageId('fish') ? 'fish' : 'shellscript'; - case 'zsh': - return this._languageService.isRegisteredLanguageId('zsh') ? 'zsh' : 'shellscript'; - case 'bash': - return this._languageService.isRegisteredLanguageId('bash') ? 'bash' : 'shellscript'; - case 'sh': - return 'shellscript'; - case 'pwsh': - return 'powershell'; - default: - return 'plaintext'; - } - } - - setValue(value: string) { - this._editor.setValue(value); - } - - getValue(): string { - return this._editor.getValue(); - } - - hide() { - hide(this._editorContainer); - } - - show() { - show(this._editorContainer); - } -} From b79e884dc16ef639963192597fab46294608217a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 7 Mar 2024 11:54:38 -0800 Subject: [PATCH 048/141] Revert "Tunnel: Add unit tests for port mapping" (#207097) delete only test --- .../electron-sandbox/resolveExternal.test.ts | 101 ------------------ 1 file changed, 101 deletions(-) delete mode 100644 src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts deleted file mode 100644 index 821367aea2f..00000000000 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; -import { RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; -import { URI } from 'vs/base/common/uri'; - -type PortMap = Record; - -class TunnelMock { - private assignedPorts: PortMap = {}; - private expectedDispose = false; - - reset(ports: PortMap) { - this.assignedPorts = ports; - } - - expectDispose() { - this.expectedDispose = true; - } - - openTunnel(_address: string, port: number): Promise { - if (!this.assignedPorts[port]) { - return Promise.reject(new Error('Unexpected tunnel request')); - } - const res: RemoteTunnel = { - localAddress: `localhost:${this.assignedPorts[port]}`, - tunnelRemoteHost: '4.3.2.1', - tunnelRemotePort: this.assignedPorts[port], - privacy: '', - dispose: () => { - assert(this.expectedDispose, 'Unexpected dispose'); - this.expectedDispose = false; - return Promise.resolve(); - } - }; - delete this.assignedPorts[port]; - return Promise.resolve(res); - } - - validate() { - try { - assert(Object.keys(this.assignedPorts).length === 0, 'Expected tunnel to be used'); - assert(!this.expectedDispose, 'Expected dispose to be called'); - } finally { - this.expectedDispose = false; - } - } -} - -suite('NativeWindow:resolveExternal', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - const tunnelMock = new TunnelMock(); - - async function doTest(uri: string, ports: PortMap = {}, expectedUri?: string) { - tunnelMock.reset(ports); - const res = await NativeWindow.prototype.resolveExternalUri.call(tunnelMock, URI.parse(uri), { - allowTunneling: true, - openExternal: true - }); - assert.strictEqual(!expectedUri, !res, `Expected URI ${expectedUri} but got ${res}`); - if (expectedUri && res) { - assert.strictEqual(res.resolved.toString(), URI.parse(expectedUri).toString()); - } - tunnelMock.validate(); - } - test('invalid', async () => { - await doTest('file:///foo.bar/baz'); - await doTest('http://foo.bar/path'); - }); - test('simple', async () => { - await doTest('http://localhost:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); - }); - test('all interfaces', async () => { - await doTest('http://0.0.0.0:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); - }); - test('changed port', async () => { - await doTest('http://localhost:1234/path', { 1234: 1235 }, 'http://localhost:1235/path'); - }); - test('query', async () => { - await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4455 }, 'http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); - }); - test('query with different port', async () => { - tunnelMock.expectDispose(); - await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4567 }); - }); - test('both url and query', async () => { - await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', - { 1234: 4321, 4455: 4455 }, - 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); - }); - test('both url and query, query rejected', async () => { - tunnelMock.expectDispose(); - await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', - { 1234: 4321, 4455: 5544 }, - 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); - }); -}); From a4be57ba959d172ed5fed3e3e586d4369641c022 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 7 Mar 2024 20:57:25 +0100 Subject: [PATCH 049/141] watcher - fix crash when rewatching a file recursively (#207102) --- .../node/watcher/parcel/parcelWatcher.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index d1b978043ed..c51b58db863 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -476,15 +476,17 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Watcher path came back! Restart watching... for (const { resource, type } of changes) { if (resource.fsPath === watcher.request.path && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { - this.warn('Watcher restarts because watched path got created again', watcher); + if (this.isPathValid(watcher.request.path)) { + this.warn('Watcher restarts because watched path got created again', watcher); - // Stop watching that parent folder - nodeWatcher.dispose(); + // Stop watching that parent folder + nodeWatcher.dispose(); - // Restart the file watching - this.restartWatching(watcher); + // Restart the file watching + this.restartWatching(watcher); - break; + break; + } } } }, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); @@ -628,19 +630,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Check for invalid paths - if (validatePaths) { - try { - const stat = statSync(request.path); - if (!stat.isDirectory()) { - this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`); - - continue; - } - } catch (error) { - this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`); - - continue; - } + if (validatePaths && !this.isPathValid(request.path)) { + continue; } requestTrie.set(request.path, request); @@ -652,6 +643,23 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return normalizedRequests; } + private isPathValid(path: string): boolean { + try { + const stat = statSync(path); + if (!stat.isDirectory()) { + this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`); + + return false; + } + } catch (error) { + this.trace(`ignoring a path for watching who's stat info failed to resolve: ${path} (error: ${error})`); + + return false; + } + + return true; + } + async setVerboseLogging(enabled: boolean): Promise { this.verboseLogging = enabled; } From 1f0e830c2bf936ad2bf5767b0777b7a6fb3f943b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 12:19:21 -0800 Subject: [PATCH 050/141] fix #207089 --- src/vs/workbench/contrib/terminal/browser/terminalEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 0ea8218fcce..900fbaf9fed 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -103,7 +103,7 @@ export class TerminalEditor extends EditorPane { override focus() { super.focus(); - this._editorInput?.terminalInstance?.focus(); + this._editorInput?.terminalInstance?.focus(true); } // eslint-disable-next-line @typescript-eslint/naming-convention From 978d141c99311ba856ff81aeeca6b5426f084a02 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 12:39:46 -0800 Subject: [PATCH 051/141] get code block to render --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index f06f0a9e5a4..37b54b0c01f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -874,7 +874,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Thu, 7 Mar 2024 12:59:59 -0800 Subject: [PATCH 052/141] Pick up latest TS for building VS Code (#207096) --- build/azure-pipelines/common/retry.js | 3 +- build/azure-pipelines/common/sign.js | 4 +- build/lib/asar.js | 3 +- build/lib/builtInExtensions.js | 5 +- build/lib/bundle.js | 3 +- build/lib/compilation.js | 8 ++-- build/lib/dependencies.js | 3 +- build/lib/extensions.js | 17 ++++--- build/lib/fetch.js | 7 ++- build/lib/getVersion.js | 3 +- build/lib/git.js | 3 +- build/lib/i18n.js | 16 +++---- build/lib/monaco-api.js | 6 +-- build/lib/nls.js | 3 +- build/lib/optimize.js | 9 ++-- build/lib/reporter.js | 3 +- build/lib/standalone.js | 5 +- build/lib/stats.js | 3 +- build/lib/stylelint/validateVariableNames.js | 3 +- build/lib/task.js | 7 ++- build/lib/treeshaking.js | 6 +-- build/lib/tsb/builder.js | 4 +- build/lib/tsb/index.js | 3 +- build/lib/util.js | 47 +++++++++---------- build/linux/debian/calculate-deps.js | 3 +- build/linux/debian/install-sysroot.js | 5 +- build/linux/debian/types.js | 3 +- build/linux/dependencies-generator.js | 3 +- build/linux/libcxx-fetcher.js | 5 +- build/linux/rpm/calculate-deps.js | 3 +- build/linux/rpm/types.js | 3 +- build/win32/explorer-appx-fetcher.js | 3 +- .../src/tsServer/protocol/protocol.d.ts | 4 +- package.json | 2 +- yarn.lock | 8 ++-- 35 files changed, 95 insertions(+), 121 deletions(-) diff --git a/build/azure-pipelines/common/retry.js b/build/azure-pipelines/common/retry.js index 7b90b0cac5b..91f60bf24b2 100644 --- a/build/azure-pipelines/common/retry.js +++ b/build/azure-pipelines/common/retry.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.retry = void 0; +exports.retry = retry; async function retry(fn) { let lastError; for (let run = 1; run <= 10; run++) { @@ -24,5 +24,4 @@ async function retry(fn) { console.error(`Too many retries, aborting.`); throw lastError; } -exports.retry = retry; //# sourceMappingURL=retry.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 4dba4765ff6..32996a7db03 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.main = exports.Temp = void 0; +exports.Temp = void 0; +exports.main = main; const cp = require("child_process"); const fs = require("fs"); const crypto = require("crypto"); @@ -164,7 +165,6 @@ function main([esrpCliPath, type, cert, username, password, folderPath, pattern] process.exit(1); } } -exports.main = main; if (require.main === module) { main(process.argv.slice(2)); process.exit(0); diff --git a/build/lib/asar.js b/build/lib/asar.js index cadb9ab974d..31845f2f2dd 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAsar = void 0; +exports.createAsar = createAsar; const path = require("path"); const es = require("event-stream"); const pickle = require('chromium-pickle-js'); @@ -115,5 +115,4 @@ function createAsar(folderPath, unpackGlobs, destFilename) { } }); } -exports.createAsar = createAsar; //# sourceMappingURL=asar.js.map \ No newline at end of file diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 1b0adc48d4c..463ce16e18d 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getBuiltInExtensions = exports.getExtensionStream = void 0; +exports.getExtensionStream = getExtensionStream; +exports.getBuiltInExtensions = getBuiltInExtensions; const fs = require("fs"); const path = require("path"); const os = require("os"); @@ -58,7 +59,6 @@ function getExtensionStream(extension) { } return getExtensionDownloadStream(extension); } -exports.getExtensionStream = getExtensionStream; function syncMarketplaceExtension(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; const source = ansiColors.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); @@ -127,7 +127,6 @@ function getBuiltInExtensions() { .on('end', resolve); }); } -exports.getBuiltInExtensions = getBuiltInExtensions; if (require.main === module) { getBuiltInExtensions().then(() => process.exit(0)).catch(err => { console.error(err); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 5d3ee9d5118..61d9f015624 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.bundle = void 0; +exports.bundle = bundle; const fs = require("fs"); const path = require("path"); const vm = require("vm"); @@ -78,7 +78,6 @@ function bundle(entryPoints, config, callback) { }); }, (err) => callback(err, null)); } -exports.bundle = bundle; function emitEntryPoints(modules, entryPoints) { const modulesMap = {}; modules.forEach((m) => { diff --git a/build/lib/compilation.js b/build/lib/compilation.js index e7a460de7d0..85cd722dbf3 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -4,7 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = exports.watchTask = exports.compileTask = exports.transpileTask = void 0; +exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; +exports.transpileTask = transpileTask; +exports.compileTask = compileTask; +exports.watchTask = watchTask; const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); @@ -96,7 +99,6 @@ function transpileTask(src, out, swc) { task.taskName = `transpile-${path.basename(src)}`; return task; } -exports.transpileTask = transpileTask; function compileTask(src, out, build, options = {}) { const task = () => { if (os.totalmem() < 4_000_000_000) { @@ -137,7 +139,6 @@ function compileTask(src, out, build, options = {}) { task.taskName = `compile-${path.basename(src)}`; return task; } -exports.compileTask = compileTask; function watchTask(out, build) { const task = () => { const compile = createCompile('src', build, false, false); @@ -153,7 +154,6 @@ function watchTask(out, build) { task.taskName = `watch-${path.basename(out)}`; return task; } -exports.watchTask = watchTask; const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); class MonacoGenerator { _isWatch; diff --git a/build/lib/dependencies.js b/build/lib/dependencies.js index 64087a9ac17..1f2dd75d68c 100644 --- a/build/lib/dependencies.js +++ b/build/lib/dependencies.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getProductionDependencies = void 0; +exports.getProductionDependencies = getProductionDependencies; const fs = require("fs"); const path = require("path"); const cp = require("child_process"); @@ -69,7 +69,6 @@ function getProductionDependencies(folderPath) { } return [...new Set(result)]; } -exports.getProductionDependencies = getProductionDependencies; if (require.main === module) { console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); } diff --git a/build/lib/extensions.js b/build/lib/extensions.js index c81568c7275..6a6c0a7b4cd 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,7 +4,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromGithub = exports.fromMarketplace = void 0; +exports.fromMarketplace = fromMarketplace; +exports.fromGithub = fromGithub; +exports.packageLocalExtensionsStream = packageLocalExtensionsStream; +exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; +exports.scanBuiltinExtensions = scanBuiltinExtensions; +exports.translatePackageJSON = translatePackageJSON; +exports.webpackExtensions = webpackExtensions; +exports.buildExtensionMedia = buildExtensionMedia; const es = require("event-stream"); const fs = require("fs"); const cp = require("child_process"); @@ -213,7 +220,6 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } -exports.fromMarketplace = fromMarketplace; function fromGithub({ name, version, repo, sha256, metadata }) { const json = require('gulp-json-editor'); fancyLog('Downloading extension from GH:', ansiColors.yellow(`${name}@${version}`), '...'); @@ -232,7 +238,6 @@ function fromGithub({ name, version, repo, sha256, metadata }) { .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } -exports.fromGithub = fromGithub; const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', @@ -306,7 +311,6 @@ function packageLocalExtensionsStream(forWeb, disableMangle) { return (result .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageLocalExtensionsStream = packageLocalExtensionsStream; function packageMarketplaceExtensionsStream(forWeb) { const marketplaceExtensionsDescriptions = [ ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), @@ -325,7 +329,6 @@ function packageMarketplaceExtensionsStream(forWeb) { return (marketplaceExtensionsStream .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { @@ -361,7 +364,6 @@ function scanBuiltinExtensions(extensionsRoot, exclude = []) { return scannedExtensions; } } -exports.scanBuiltinExtensions = scanBuiltinExtensions; function translatePackageJSON(packageJSON, packageNLSPath) { const CharCode_PC = '%'.charCodeAt(0); const packageNls = JSON.parse(fs.readFileSync(packageNLSPath).toString()); @@ -385,7 +387,6 @@ function translatePackageJSON(packageJSON, packageNLSPath) { translate(packageJSON); return packageJSON; } -exports.translatePackageJSON = translatePackageJSON; const extensionsPath = path.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ @@ -459,7 +460,6 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { } }); } -exports.webpackExtensions = webpackExtensions; async function esbuildExtensions(taskName, isWatch, scripts) { function reporter(stdError, script) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); @@ -500,5 +500,4 @@ async function buildExtensionMedia(isWatch, outputRoot) { outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined }))); } -exports.buildExtensionMedia = buildExtensionMedia; //# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/build/lib/fetch.js b/build/lib/fetch.js index ba23e78257c..2fed63bca0e 100644 --- a/build/lib/fetch.js +++ b/build/lib/fetch.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.fetchGithub = exports.fetchUrl = exports.fetchUrls = void 0; +exports.fetchUrls = fetchUrls; +exports.fetchUrl = fetchUrl; +exports.fetchGithub = fetchGithub; const es = require("event-stream"); const VinylFile = require("vinyl"); const log = require("fancy-log"); @@ -30,7 +32,6 @@ function fetchUrls(urls, options) { }); })); } -exports.fetchUrls = fetchUrls; async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); try { @@ -94,7 +95,6 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { throw e; } } -exports.fetchUrl = fetchUrl; const ghApiHeaders = { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'VSCode Build', @@ -135,5 +135,4 @@ function fetchGithub(repo, options) { } })); } -exports.fetchGithub = fetchGithub; //# sourceMappingURL=fetch.js.map \ No newline at end of file diff --git a/build/lib/getVersion.js b/build/lib/getVersion.js index abf05e93210..b50ead538a2 100644 --- a/build/lib/getVersion.js +++ b/build/lib/getVersion.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = void 0; +exports.getVersion = getVersion; const git = require("./git"); function getVersion(root) { let version = process.env['BUILD_SOURCEVERSION']; @@ -13,5 +13,4 @@ function getVersion(root) { } return version; } -exports.getVersion = getVersion; //# sourceMappingURL=getVersion.js.map \ No newline at end of file diff --git a/build/lib/git.js b/build/lib/git.js index a8e712ed070..798a408bdb9 100644 --- a/build/lib/git.js +++ b/build/lib/git.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = void 0; +exports.getVersion = getVersion; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -51,5 +51,4 @@ function getVersion(repo) { } return refs[ref]; } -exports.getVersion = getVersion; //# sourceMappingURL=git.js.map \ No newline at end of file diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 1844af139c5..c33994987f0 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -4,7 +4,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.EXTERNAL_EXTENSIONS = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.EXTERNAL_EXTENSIONS = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.processNlsFiles = processNlsFiles; +exports.getResource = getResource; +exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; +exports.createXlfFilesForExtensions = createXlfFilesForExtensions; +exports.createXlfFilesForIsl = createXlfFilesForIsl; +exports.prepareI18nPackFiles = prepareI18nPackFiles; +exports.prepareIslFiles = prepareIslFiles; const path = require("path"); const fs = require("fs"); const event_stream_1 = require("event-stream"); @@ -423,7 +430,6 @@ function processNlsFiles(opts) { this.queue(file); }); } -exports.processNlsFiles = processNlsFiles; const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench', extensionsProject = 'vscode-extensions', setupProject = 'vscode-setup', serverProject = 'vscode-server'; function getResource(sourceFile) { let resource; @@ -458,7 +464,6 @@ function getResource(sourceFile) { } throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); } -exports.getResource = getResource; function createXlfFilesForCoreBundle() { return (0, event_stream_1.through)(function (file) { const basename = path.basename(file.path); @@ -506,7 +511,6 @@ function createXlfFilesForCoreBundle() { } }); } -exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder) { const prefix = prefixWithBuildFolder ? '.build/' : ''; return gulp @@ -653,7 +657,6 @@ function createXlfFilesForExtensions() { } }); } -exports.createXlfFilesForExtensions = createXlfFilesForExtensions; function createXlfFilesForIsl() { return (0, event_stream_1.through)(function (file) { let projectName, resourceFile; @@ -704,7 +707,6 @@ function createXlfFilesForIsl() { this.queue(xlfFile); }); } -exports.createXlfFilesForIsl = createXlfFilesForIsl; function createI18nFile(name, messages) { const result = Object.create(null); result[''] = [ @@ -793,7 +795,6 @@ function prepareI18nPackFiles(resultingTranslationPaths) { }); }); } -exports.prepareI18nPackFiles = prepareI18nPackFiles; function prepareIslFiles(language, innoSetupConfig) { const parsePromises = []; return (0, event_stream_1.through)(function (xlf) { @@ -816,7 +817,6 @@ function prepareIslFiles(language, innoSetupConfig) { }); }); } -exports.prepareIslFiles = prepareIslFiles; function createIslFile(name, messages, language, innoSetup) { const content = []; let originalContent; diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js index 6512b6ae886..2052806c46b 100644 --- a/build/lib/monaco-api.js +++ b/build/lib/monaco-api.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = exports.run3 = exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; +exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; +exports.run3 = run3; +exports.execute = execute; const fs = require("fs"); const path = require("path"); const fancyLog = require("fancy-log"); @@ -559,7 +561,6 @@ function run3(resolver) { const sourceFileGetter = (moduleId) => resolver.getDeclarationSourceFile(moduleId); return _run(resolver.ts, sourceFileGetter); } -exports.run3 = run3; class TypeScriptLanguageServiceHost { _ts; _libs; @@ -623,5 +624,4 @@ function execute() { } return r; } -exports.execute = execute; //# sourceMappingURL=monaco-api.js.map \ No newline at end of file diff --git a/build/lib/nls.js b/build/lib/nls.js index 982f74bcf4d..48ca84f2433 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.nls = void 0; +exports.nls = nls; const lazy = require("lazy.js"); const event_stream_1 = require("event-stream"); const File = require("vinyl"); @@ -74,7 +74,6 @@ function nls() { })); return (0, event_stream_1.duplex)(input, output); } -exports.nls = nls; function isImportNode(ts, node) { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 9dff0859acc..237f2bc20e8 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -4,7 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.minifyTask = exports.optimizeTask = exports.optimizeLoaderTask = exports.loaderConfig = void 0; +exports.loaderConfig = loaderConfig; +exports.optimizeLoaderTask = optimizeLoaderTask; +exports.optimizeTask = optimizeTask; +exports.minifyTask = minifyTask; const es = require("event-stream"); const gulp = require("gulp"); const concat = require("gulp-concat"); @@ -33,7 +36,6 @@ function loaderConfig() { result['vs/css'] = { inlineResources: true }; return result; } -exports.loaderConfig = loaderConfig; const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; function loaderPlugin(src, base, amdModuleId) { return (gulp @@ -223,7 +225,6 @@ function optimizeManualTask(options) { function optimizeLoaderTask(src, out, bundleLoader, bundledFileHeader = '', externalLoaderInfo) { return () => loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo).pipe(gulp.dest(out)); } -exports.optimizeLoaderTask = optimizeLoaderTask; function optimizeTask(opts) { return function () { const optimizers = [optimizeAMDTask(opts.amd)]; @@ -236,7 +237,6 @@ function optimizeTask(opts) { return es.merge(...optimizers).pipe(gulp.dest(opts.out)); }; } -exports.optimizeTask = optimizeTask; function minifyTask(src, sourceMapBaseUrl) { const esbuild = require('esbuild'); const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; @@ -284,5 +284,4 @@ function minifyTask(src, sourceMapBaseUrl) { }), gulp.dest(src + '-min'), (err) => cb(err)); }; } -exports.minifyTask = minifyTask; //# sourceMappingURL=optimize.js.map \ No newline at end of file diff --git a/build/lib/reporter.js b/build/lib/reporter.js index 305d7364287..9d4a1b4fd79 100644 --- a/build/lib/reporter.js +++ b/build/lib/reporter.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createReporter = void 0; +exports.createReporter = createReporter; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); @@ -99,5 +99,4 @@ function createReporter(id) { }; return result; } -exports.createReporter = createReporter; //# sourceMappingURL=reporter.js.map \ No newline at end of file diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 4ddf88ed223..dbc47db0833 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createESMSourcesAndResources2 = exports.extractEditor = void 0; +exports.extractEditor = extractEditor; +exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; const fs = require("fs"); const path = require("path"); const tss = require("./treeshaking"); @@ -111,7 +112,6 @@ function extractEditor(options) { 'vs/nls.mock.ts', ].forEach(copyFile); } -exports.extractEditor = extractEditor; function createESMSourcesAndResources2(options) { const ts = require('typescript'); const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); @@ -251,7 +251,6 @@ function createESMSourcesAndResources2(options) { } } } -exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; diff --git a/build/lib/stats.js b/build/lib/stats.js index d923bb809da..e089cb0c1b4 100644 --- a/build/lib/stats.js +++ b/build/lib/stats.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createStatsStream = void 0; +exports.createStatsStream = createStatsStream; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); @@ -73,5 +73,4 @@ function createStatsStream(group, log) { this.emit('end'); }); } -exports.createStatsStream = createStatsStream; //# sourceMappingURL=stats.js.map \ No newline at end of file diff --git a/build/lib/stylelint/validateVariableNames.js b/build/lib/stylelint/validateVariableNames.js index 2367fb94c2e..57b2aad957f 100644 --- a/build/lib/stylelint/validateVariableNames.js +++ b/build/lib/stylelint/validateVariableNames.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVariableNameValidator = void 0; +exports.getVariableNameValidator = getVariableNameValidator; const fs_1 = require("fs"); const path = require("path"); const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; @@ -30,5 +30,4 @@ function getVariableNameValidator() { } }; } -exports.getVariableNameValidator = getVariableNameValidator; //# sourceMappingURL=validateVariableNames.js.map \ No newline at end of file diff --git a/build/lib/task.js b/build/lib/task.js index 6b040a75698..597b2a0d397 100644 --- a/build/lib/task.js +++ b/build/lib/task.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.define = exports.parallel = exports.series = void 0; +exports.series = series; +exports.parallel = parallel; +exports.define = define; const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); function _isPromise(p) { @@ -67,7 +69,6 @@ function series(...tasks) { result._tasks = tasks; return result; } -exports.series = series; function parallel(...tasks) { const result = async () => { await Promise.all(tasks.map(t => _execute(t))); @@ -75,7 +76,6 @@ function parallel(...tasks) { result._tasks = tasks; return result; } -exports.parallel = parallel; function define(name, task) { if (task._tasks) { // This is a composite task @@ -94,5 +94,4 @@ function define(name, task) { task.displayName = name; return task; } -exports.define = define; //# sourceMappingURL=task.js.map \ No newline at end of file diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 51c610ecda2..c8e95511877 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.shake = exports.toStringShakeLevel = exports.ShakeLevel = void 0; +exports.ShakeLevel = void 0; +exports.toStringShakeLevel = toStringShakeLevel; +exports.shake = shake; const fs = require("fs"); const path = require("path"); const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); @@ -24,7 +26,6 @@ function toStringShakeLevel(shakeLevel) { return 'ClassMembers (2)'; } } -exports.toStringShakeLevel = toStringShakeLevel; function printDiagnostics(options, diagnostics) { for (const diag of diagnostics) { let result = ''; @@ -61,7 +62,6 @@ function shake(options) { markNodes(ts, languageService, options); return generateResult(ts, languageService, options.shakeLevel); } -exports.shake = shake; //#region Discovery, LanguageService & Setup function createTypeScriptLanguageService(ts, options) { // Discover referenced files diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index e87945ea9cc..fc74bfa8acc 100644 --- a/build/lib/tsb/builder.js +++ b/build/lib/tsb/builder.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createTypeScriptBuilder = exports.CancellationToken = void 0; +exports.CancellationToken = void 0; +exports.createTypeScriptBuilder = createTypeScriptBuilder; const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); @@ -364,7 +365,6 @@ function createTypeScriptBuilder(config, projectFile, cmd) { languageService: service }; } -exports.createTypeScriptBuilder = createTypeScriptBuilder; class ScriptSnapshot { _text; _mtime; diff --git a/build/lib/tsb/index.js b/build/lib/tsb/index.js index 47f26bc8178..8b8116d5a49 100644 --- a/build/lib/tsb/index.js +++ b/build/lib/tsb/index.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.create = void 0; +exports.create = create; const Vinyl = require("vinyl"); const through = require("through"); const builder = require("./builder"); @@ -132,5 +132,4 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) }; return result; } -exports.create = create; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/build/lib/util.js b/build/lib/util.js index 388ef5df948..ed52776c2c0 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -4,7 +4,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildWebNodePaths = exports.createExternalLoaderConfig = exports.acquireWebNodePaths = exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.ensureDir = exports.rreddir = exports.rimraf = exports.rewriteSourceMappingURL = exports.appendOwnPathSourceURL = exports.$if = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.debounce = exports.incremental = void 0; +exports.incremental = incremental; +exports.debounce = debounce; +exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; +exports.setExecutableBit = setExecutableBit; +exports.toFileUri = toFileUri; +exports.skipDirectories = skipDirectories; +exports.cleanNodeModules = cleanNodeModules; +exports.loadSourcemaps = loadSourcemaps; +exports.stripSourceMappingURL = stripSourceMappingURL; +exports.$if = $if; +exports.appendOwnPathSourceURL = appendOwnPathSourceURL; +exports.rewriteSourceMappingURL = rewriteSourceMappingURL; +exports.rimraf = rimraf; +exports.rreddir = rreddir; +exports.ensureDir = ensureDir; +exports.rebase = rebase; +exports.filter = filter; +exports.versionStringToNumber = versionStringToNumber; +exports.streamToPromise = streamToPromise; +exports.getElectronVersion = getElectronVersion; +exports.acquireWebNodePaths = acquireWebNodePaths; +exports.createExternalLoaderConfig = createExternalLoaderConfig; +exports.buildWebNodePaths = buildWebNodePaths; const es = require("event-stream"); const _debounce = require("debounce"); const _filter = require("gulp-filter"); @@ -54,7 +76,6 @@ function incremental(streamProvider, initial, supportsCancellation) { }); return es.duplex(input, output); } -exports.incremental = incremental; function debounce(task, duration = 500) { const input = es.through(); const output = es.through(); @@ -83,7 +104,6 @@ function debounce(task, duration = 500) { }); return es.duplex(input, output); } -exports.debounce = debounce; function fixWin32DirectoryPermissions() { if (!/win32/.test(process.platform)) { return es.through(); @@ -95,7 +115,6 @@ function fixWin32DirectoryPermissions() { return f; }); } -exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; function setExecutableBit(pattern) { const setBit = es.mapSync(f => { if (!f.stat) { @@ -115,7 +134,6 @@ function setExecutableBit(pattern) { .pipe(filter.restore); return es.duplex(input, output); } -exports.setExecutableBit = setExecutableBit; function toFileUri(filePath) { const match = filePath.match(/^([a-z])\:(.*)$/i); if (match) { @@ -123,7 +141,6 @@ function toFileUri(filePath) { } return 'file://' + filePath.replace(/\\/g, '/'); } -exports.toFileUri = toFileUri; function skipDirectories() { return es.mapSync(f => { if (!f.isDirectory()) { @@ -131,7 +148,6 @@ function skipDirectories() { } }); } -exports.skipDirectories = skipDirectories; function cleanNodeModules(rulePath) { const rules = fs.readFileSync(rulePath, 'utf8') .split(/\r?\n/g) @@ -143,7 +159,6 @@ function cleanNodeModules(rulePath) { const output = es.merge(input.pipe(_filter(['**', ...excludes])), input.pipe(_filter(includes))); return es.duplex(input, output); } -exports.cleanNodeModules = cleanNodeModules; function loadSourcemaps() { const input = es.through(); const output = input @@ -185,7 +200,6 @@ function loadSourcemaps() { })); return es.duplex(input, output); } -exports.loadSourcemaps = loadSourcemaps; function stripSourceMappingURL() { const input = es.through(); const output = input @@ -196,7 +210,6 @@ function stripSourceMappingURL() { })); return es.duplex(input, output); } -exports.stripSourceMappingURL = stripSourceMappingURL; /** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */ function $if(test, onTrue, onFalse = es.through()) { if (typeof test === 'boolean') { @@ -204,7 +217,6 @@ function $if(test, onTrue, onFalse = es.through()) { } return ternaryStream(test, onTrue, onFalse); } -exports.$if = $if; /** Operator that appends the js files' original path a sourceURL, so debug locations map */ function appendOwnPathSourceURL() { const input = es.through(); @@ -218,7 +230,6 @@ function appendOwnPathSourceURL() { })); return es.duplex(input, output); } -exports.appendOwnPathSourceURL = appendOwnPathSourceURL; function rewriteSourceMappingURL(sourceMappingURLBase) { const input = es.through(); const output = input @@ -230,7 +241,6 @@ function rewriteSourceMappingURL(sourceMappingURLBase) { })); return es.duplex(input, output); } -exports.rewriteSourceMappingURL = rewriteSourceMappingURL; function rimraf(dir) { const result = () => new Promise((c, e) => { let retries = 0; @@ -250,7 +260,6 @@ function rimraf(dir) { result.taskName = `clean-${path.basename(dir).toLowerCase()}`; return result; } -exports.rimraf = rimraf; function _rreaddir(dirPath, prepend, result) { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { @@ -267,7 +276,6 @@ function rreddir(dirPath) { _rreaddir(dirPath, '', result); return result; } -exports.rreddir = rreddir; function ensureDir(dirPath) { if (fs.existsSync(dirPath)) { return; @@ -275,14 +283,12 @@ function ensureDir(dirPath) { ensureDir(path.dirname(dirPath)); fs.mkdirSync(dirPath); } -exports.ensureDir = ensureDir; function rebase(count) { return rename(f => { const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; f.dirname = parts.slice(count).join(path.sep); }); } -exports.rebase = rebase; function filter(fn) { const result = es.through(function (data) { if (fn(data)) { @@ -295,7 +301,6 @@ function filter(fn) { result.restore = es.through(); return result; } -exports.filter = filter; function versionStringToNumber(versionStr) { const semverRegex = /(\d+)\.(\d+)\.(\d+)/; const match = versionStr.match(semverRegex); @@ -304,21 +309,18 @@ function versionStringToNumber(versionStr) { } return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); } -exports.versionStringToNumber = versionStringToNumber; function streamToPromise(stream) { return new Promise((c, e) => { stream.on('error', err => e(err)); stream.on('end', () => c()); }); } -exports.streamToPromise = streamToPromise; function getElectronVersion() { const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); const electronVersion = /^target "(.*)"$/m.exec(yarnrc)[1]; const msBuildId = /^ms_build_id "(.*)"$/m.exec(yarnrc)[1]; return { electronVersion, msBuildId }; } -exports.getElectronVersion = getElectronVersion; function acquireWebNodePaths() { const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); @@ -367,7 +369,6 @@ function acquireWebNodePaths() { nodePaths['@microsoft/applicationinsights-core-js'] = 'browser/applicationinsights-core-js.min.js'; return nodePaths; } -exports.acquireWebNodePaths = acquireWebNodePaths; function createExternalLoaderConfig(webEndpoint, commit, quality) { if (!webEndpoint || !commit || !quality) { return undefined; @@ -384,7 +385,6 @@ function createExternalLoaderConfig(webEndpoint, commit, quality) { }; return externalLoaderConfig; } -exports.createExternalLoaderConfig = createExternalLoaderConfig; function buildWebNodePaths(outDir) { const result = () => new Promise((resolve, _) => { const root = path.join(__dirname, '..', '..'); @@ -405,5 +405,4 @@ function buildWebNodePaths(outDir) { result.taskName = 'build-web-node-paths'; return result; } -exports.buildWebNodePaths = buildWebNodePaths; //# sourceMappingURL=util.js.map \ No newline at end of file diff --git a/build/linux/debian/calculate-deps.js b/build/linux/debian/calculate-deps.js index 6304df9edda..bbcb6bfc3de 100644 --- a/build/linux/debian/calculate-deps.js +++ b/build/linux/debian/calculate-deps.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = void 0; +exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const os_1 = require("os"); @@ -17,7 +17,6 @@ function generatePackageDeps(files, arch, chromiumSysroot, vscodeSysroot) { dependencies.push(additionalDepsSet); return dependencies; } -exports.generatePackageDeps = generatePackageDeps; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/calculate_package_deps.py. function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) { try { diff --git a/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js index d637fce3ca6..feca7d3fa9d 100644 --- a/build/linux/debian/install-sysroot.js +++ b/build/linux/debian/install-sysroot.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getChromiumSysroot = exports.getVSCodeSysroot = void 0; +exports.getVSCodeSysroot = getVSCodeSysroot; +exports.getChromiumSysroot = getChromiumSysroot; const child_process_1 = require("child_process"); const os_1 = require("os"); const fs = require("fs"); @@ -156,7 +157,6 @@ async function getVSCodeSysroot(arch) { fs.writeFileSync(stamp, expectedName); return result; } -exports.getVSCodeSysroot = getVSCodeSysroot; async function getChromiumSysroot(arch) { const sysrootJSONUrl = `https://raw.githubusercontent.com/electron/electron/v${getElectronVersion().electronVersion}/script/sysroots.json`; const sysrootDictLocation = `${(0, os_1.tmpdir)()}/sysroots.json`; @@ -214,5 +214,4 @@ async function getChromiumSysroot(arch) { fs.writeFileSync(stamp, url); return sysroot; } -exports.getChromiumSysroot = getChromiumSysroot; //# sourceMappingURL=install-sysroot.js.map \ No newline at end of file diff --git a/build/linux/debian/types.js b/build/linux/debian/types.js index 2cd177c34a8..ce21d50e1a9 100644 --- a/build/linux/debian/types.js +++ b/build/linux/debian/types.js @@ -4,9 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isDebianArchString = void 0; +exports.isDebianArchString = isDebianArchString; function isDebianArchString(s) { return ['amd64', 'armhf', 'arm64'].includes(s); } -exports.isDebianArchString = isDebianArchString; //# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 58db0f4af51..80c247d1129 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getDependencies = void 0; +exports.getDependencies = getDependencies; const child_process_1 = require("child_process"); const path = require("path"); const install_sysroot_1 = require("./debian/install-sysroot"); @@ -92,7 +92,6 @@ async function getDependencies(packageType, buildDir, applicationName, arch) { } return sortedDependencies; } -exports.getDependencies = getDependencies; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. function mergePackageDeps(inputDeps) { const requires = new Set(); diff --git a/build/linux/libcxx-fetcher.js b/build/linux/libcxx-fetcher.js index 1e195ba1fac..cfdc9498502 100644 --- a/build/linux/libcxx-fetcher.js +++ b/build/linux/libcxx-fetcher.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadLibcxxObjects = exports.downloadLibcxxHeaders = void 0; +exports.downloadLibcxxHeaders = downloadLibcxxHeaders; +exports.downloadLibcxxObjects = downloadLibcxxObjects; // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. const fs = require("fs"); const path = require("path"); @@ -29,7 +30,6 @@ async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { d(`unpacking ${lib_name}_headers from ${headers}`); await extract(headers, { dir: outDir }); } -exports.downloadLibcxxHeaders = downloadLibcxxHeaders; async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64') { if (await fs.existsSync(path.resolve(outDir, 'libc++.a'))) { return; @@ -47,7 +47,6 @@ async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64' d(`unpacking libcxx-objects from ${objects}`); await extract(objects, { dir: outDir }); } -exports.downloadLibcxxObjects = downloadLibcxxObjects; async function main() { const libcxxObjectsDirPath = process.env['VSCODE_LIBCXX_OBJECTS_DIR']; const libcxxHeadersDownloadDir = process.env['VSCODE_LIBCXX_HEADERS_DIR']; diff --git a/build/linux/rpm/calculate-deps.js b/build/linux/rpm/calculate-deps.js index ac870e4a546..b19e26f1854 100644 --- a/build/linux/rpm/calculate-deps.js +++ b/build/linux/rpm/calculate-deps.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = void 0; +exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const dep_lists_1 = require("./dep-lists"); @@ -14,7 +14,6 @@ function generatePackageDeps(files) { dependencies.push(additionalDepsSet); return dependencies; } -exports.generatePackageDeps = generatePackageDeps; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/calculate_package_deps.py. function calculatePackageDeps(binaryPath) { try { diff --git a/build/linux/rpm/types.js b/build/linux/rpm/types.js index 6dba7cf38d1..a20b9c2fe02 100644 --- a/build/linux/rpm/types.js +++ b/build/linux/rpm/types.js @@ -4,9 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isRpmArchString = void 0; +exports.isRpmArchString = isRpmArchString; function isRpmArchString(s) { return ['x86_64', 'armv7hl', 'aarch64'].includes(s); } -exports.isRpmArchString = isRpmArchString; //# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/build/win32/explorer-appx-fetcher.js b/build/win32/explorer-appx-fetcher.js index d618c21674a..554b449d872 100644 --- a/build/win32/explorer-appx-fetcher.js +++ b/build/win32/explorer-appx-fetcher.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadExplorerAppx = void 0; +exports.downloadExplorerAppx = downloadExplorerAppx; const fs = require("fs"); const debug = require("debug"); const extract = require("extract-zip"); @@ -36,7 +36,6 @@ async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x6 d(`unpacking from ${fileName}`); await extract(artifact, { dir: fs.realpathSync(outDir) }); } -exports.downloadExplorerAppx = downloadExplorerAppx; async function main(outputDir) { const arch = process.env['VSCODE_ARCH']; if (!outputDir) { diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index b8d863ebb1a..45e09d63481 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript/lib/tsserverlibrary'; +import type ts from '../../../../node_modules/typescript/lib/typescript'; export = ts.server.protocol; @@ -11,7 +11,7 @@ declare enum ServerType { Semantic = 'semantic', } -declare module 'typescript/lib/tsserverlibrary' { +declare module '../../../../node_modules/typescript/lib/typescript' { namespace server.protocol { type TextInsertion = ts.TextInsertion; type ScriptElementKind = ts.ScriptElementKind; diff --git a/package.json b/package.json index 24bb6558dee..fae4fcc57a8 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240226", + "typescript": "^5.5.0-dev.20240307", "typescript-formatter": "7.1.0", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index c96688f58da..a7e82c60be5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9643,10 +9643,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240226: - version "5.5.0-dev.20240226" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240226.tgz#b571688666f07e4d7db4c9863f3ee1401e161a7a" - integrity sha512-mLY9/pjzSCr7JLkMKHS3KQUKX+LPO9WWjiR+mRcWKcskSdMBZ0j1TPhk/zUyuBklOf3YX4orkvamNiZWZEK0CQ== +typescript@^5.5.0-dev.20240307: + version "5.5.0-dev.20240307" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240307.tgz#dcd10b4a4d5a274f40cdfa91c75ae7a940ade469" + integrity sha512-9QgBAVHg1keeajSXIqq1ngFy3yTA0um5YWoLNVSfPtIwqqYoVqRjmh4oJKJL4YM15C2HY8IXL/UEUBIjXm/tjg== typical@^4.0.0: version "4.0.0" From 5ae204d5ad57beb70812a65bfd47b84499372753 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 22:06:39 +0100 Subject: [PATCH 053/141] Fix Drag And Drop in Activity Bar (#207109) fixes #206878 --- src/vs/workbench/browser/parts/compositeBar.ts | 3 ++- src/vs/workbench/browser/parts/paneCompositeBar.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 27d0b3738ea..a9fc39c84ed 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -37,6 +37,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { constructor( private viewDescriptorService: IViewDescriptorService, private targetContainerLocation: ViewContainerLocation, + private orientation: ActionsOrientation, private openComposite: (id: string, focus?: boolean) => Promise, private moveComposite: (from: string, to: string, before?: Before2D) => void, private getItems: () => ICompositeBarItem[] @@ -93,7 +94,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } const items = this.getItems(); - const before = this.targetContainerLocation === ViewContainerLocation.Panel ? before2d?.horizontallyBefore : before2d?.verticallyBefore; + const before = this.orientation === ActionsOrientation.HORIZONTAL ? before2d?.horizontallyBefore : before2d?.verticallyBefore; return items.filter(item => item.visible).findIndex(item => item.id === targetId) + (before ? 0 : 1); } diff --git a/src/vs/workbench/browser/parts/paneCompositeBar.ts b/src/vs/workbench/browser/parts/paneCompositeBar.ts index 8fe901376cc..94dce01b958 100644 --- a/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/src/vs/workbench/browser/parts/paneCompositeBar.ts @@ -111,7 +111,7 @@ export class PaneCompositeBar extends Disposable { ? ViewContainerLocation.Panel : paneCompositePart.partId === Parts.AUXILIARYBAR_PART ? ViewContainerLocation.AuxiliaryBar : ViewContainerLocation.Sidebar; - this.dndHandler = new CompositeDragAndDrop(this.viewDescriptorService, this.location, + this.dndHandler = new CompositeDragAndDrop(this.viewDescriptorService, this.location, this.options.orientation, async (id: string, focus?: boolean) => { return await this.paneCompositePart.openPaneComposite(id, focus) ?? null; }, (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, this.options.orientation === ActionsOrientation.VERTICAL ? before?.verticallyBefore : before?.horizontallyBefore), () => this.compositeBar.getCompositeBarItems(), From 2356b7b15a04dcf1162683392348d6a8e11e7080 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 13:15:31 -0800 Subject: [PATCH 054/141] fix acc view actions --- .../contrib/accessibility/browser/accessibleView.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index b8411ddadfe..86f6c05bbd1 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -362,7 +362,7 @@ export class AccessibleView extends Disposable { } calculateCodeBlocks(markdown: string): void { - if (!this._currentProvider || this._currentProvider.options.id !== AccessibleViewProviderId.Chat) { + if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { @@ -519,9 +519,7 @@ export class AccessibleView extends Disposable { const verbose = this._configurationService.getValue(provider.verbositySettingKey); const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; - if (newContent && newContent !== this._currentContent && provider.options.type !== AccessibleViewType.Help && !provider.options.language || provider.options.language === 'markdown') { - this.calculateCodeBlocks(newContent); - } + this.calculateCodeBlocks(newContent); this._currentContent = newContent; this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); @@ -577,6 +575,7 @@ export class AccessibleView extends Disposable { this._contextViewService.hideContextView(); this._updateContextKeys(provider, false); this._lastProvider = undefined; + this._currentContent = undefined; }; const disposableStore = new DisposableStore(); disposableStore.add(this._editorWidget.onKeyDown((e) => { From c087be970518726e3627163e7f193f95bdd22498 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 7 Mar 2024 13:35:56 -0800 Subject: [PATCH 055/141] Adopt module 'Preserve' for TS 5.4+ (#206478) --- extensions/typescript-language-features/src/tsconfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/src/tsconfig.ts b/extensions/typescript-language-features/src/tsconfig.ts index 196cf185170..04f08a128bc 100644 --- a/extensions/typescript-language-features/src/tsconfig.ts +++ b/extensions/typescript-language-features/src/tsconfig.ts @@ -26,8 +26,8 @@ export function inferredProjectCompilerOptions( serviceConfig: TypeScriptServiceConfiguration, ): Proto.ExternalProjectCompilerOptions { const projectConfig: Proto.ExternalProjectCompilerOptions = { - module: 'ESNext' as Proto.ModuleKind, - moduleResolution: 'Node' as Proto.ModuleResolutionKind, + module: (version.gte(API.v540) ? 'Preserve' : 'ESNext') as Proto.ModuleKind, + moduleResolution: (version.gte(API.v540) ? 'Bundler' : 'Node') as Proto.ModuleResolutionKind, target: 'ES2022' as Proto.ScriptTarget, jsx: 'react' as Proto.JsxEmit, }; From ea874924d938f9a2d0c85f3af9f17788474b96e5 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 13:39:02 -0800 Subject: [PATCH 056/141] trap language for acc view codeblock --- .../workbench/contrib/accessibility/browser/accessibleView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 86f6c05bbd1..9e40c9cc8ed 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -374,8 +374,8 @@ export class AccessibleView extends Disposable { let inBlock = false; let startLine = 0; + let languageId: string | undefined; lines.forEach((line, i) => { - let languageId: string | undefined; if (!inBlock && line.startsWith('```')) { inBlock = true; startLine = i + 1; From 44f0428a895c024aad80c36369672b4ec9b4af4d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:06:14 +0100 Subject: [PATCH 057/141] Reorder Activity Bar Position Menu (#207113) Activity Bar Position Menu Order --- src/vs/workbench/browser/parts/activitybar/activitybarPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 4ee5b07e211..15e58a58179 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -388,7 +388,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 2 + order: 1 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), @@ -414,7 +414,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 1 + order: 2 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP), From 2bad96c0857c4310b4e112c9fb1b700609a6db17 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 7 Mar 2024 14:17:13 -0800 Subject: [PATCH 058/141] add navigation actions, rm toolbars, add to help dialog --- .../browser/actions/chatCodeblockActions.ts | 2 + .../chat/browser/terminalChat.ts | 2 + .../browser/terminalChatAccessibilityHelp.ts | 8 ++-- .../chat/browser/terminalChatActions.ts | 46 +++++++++++++++++++ .../chat/browser/terminalChatWidget.ts | 18 +++++++- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 266c30bfe98..d923b51fa6d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -90,6 +90,7 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', + when: ContextKeyExpr.not('terminalChatFocus') } }); } @@ -357,6 +358,7 @@ export function registerChatCodeBlockActions() { id: MenuId.ChatCodeBlock, group: 'navigation', isHiddenByDefault: true, + when: ContextKeyExpr.not('terminalChatFocus') } }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 89893d6d31e..48fa65a5f50 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -10,6 +10,8 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const enum TerminalChatCommandId { Start = 'workbench.action.terminal.chat.start', Close = 'workbench.action.terminal.chat.close', + FocusResponse = 'workbench.action.terminal.chat.focusResponse', + FocusInput = 'workbench.action.terminal.chat.focusInput', Discard = 'workbench.action.terminal.chat.discard', MakeRequest = 'workbench.action.terminal.chat.makeRequest', Cancel = 'workbench.action.terminal.chat.cancel', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index dad7747e7f1..584bdc753d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -49,12 +49,14 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const insertCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.InsertCommand)?.getAriaLabel(); const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); const startChatKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.Start)?.getAriaLabel(); + const focusResponseKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.FocusResponse)?.getAriaLabel(); + const focusInputKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.FocusInput)?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.")); content.push(localize('inlineChat.access', "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", startChatKeybinding)); content.push(makeRequestKeybinding ? localize('inlineChat.input', "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", makeRequestKeybinding) : localize('inlineChat.inputNoKb', "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.")); - content.push(localize('inlineChat.results', "A result may contain a terminal command or just a message. In either case, the result will be announced.")); - content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'If just a message comes back, it can be inspected in the accessible view ({0}).', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); - content.push(localize('inlineChat.inspectTerminalCommand', 'If a terminal command comes back, it can be inspected in an editor reached via Shift+Tab.')); + content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'The response can be inspected in the accessible view ({0}).', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(focusResponseKeybinding ? localize('inlineChat.focusResponse', 'Reach the response from the input box ({0}).', focusResponseKeybinding) : localize('inlineChat.focusResponseNoKb', 'Reach the response from the input box by tabbing or assigning a keybinding for the command: Focus Terminal Response.')); + content.push(focusInputKeybinding ? localize('inlineChat.focusInput', 'Reach the input box from the response ({0}).', focusInputKeybinding) : localize('inlineChat.focusInputNoKb', 'Reach the response from the input box by shift+tabbing or assigning a keybinding for the command: Focus Terminal Input.')); content.push(runCommandKeybinding ? localize('inlineChat.runCommand', 'With focus in the input box or command editor, the Terminal: Run Chat Command ({0}) action.', runCommandKeybinding) : localize('inlineChat.runCommandNoKb', 'Run a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); content.push(insertCommandKeybinding ? localize('inlineChat.insertCommand', 'With focus in the input box command editor, the Terminal: Insert Chat Command ({0}) action.', insertCommandKeybinding) : localize('inlineChat.insertCommandNoKb', 'Insert a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 3a46d2929c4..e0ba2d732c7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -72,6 +72,52 @@ registerActiveXtermAction({ } }); +registerActiveXtermAction({ + id: TerminalChatCommandId.FocusResponse, + title: localize2('focusTerminalResponse', 'Focus Terminal Response'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.focused + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.focusResponse(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FocusInput, + title: localize2('focusTerminalInput', 'Focus Terminal Input'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.focused + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.focus(); + } +}); + registerActiveXtermAction({ id: TerminalChatCommandId.Discard, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index dcc6e9d5d55..0e5593f71ab 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -106,6 +106,12 @@ export class TerminalChatWidget extends Disposable { focus(): void { this._inlineChatWidget.focus(); } + focusResponse(): void { + const responseElement = this._inlineChatWidget.domNode.querySelector(ChatElementSelectors.ResponseEditor) || this._inlineChatWidget.domNode.querySelector(ChatElementSelectors.ResponseMessage); + if (responseElement instanceof HTMLElement) { + responseElement.focus(); + } + } hasFocus(): boolean { return this._inlineChatWidget.hasFocus(); } @@ -117,7 +123,7 @@ export class TerminalChatWidget extends Disposable { } acceptCommand(shouldExecute: boolean): void { // Trim command to remove any whitespace, otherwise this may execute the command - const value = this._inlineChatWidget?.responseContent?.trim(); + const value = parseCodeFromBlock(this._inlineChatWidget?.responseContent?.trim()); if (!value) { return; } @@ -132,3 +138,13 @@ export class TerminalChatWidget extends Disposable { } } +function parseCodeFromBlock(block?: string): string | undefined { + const match = block?.match(/```.*?\n([\s\S]*?)```/); + return match ? match[1].trim() : undefined; +} + + +const enum ChatElementSelectors { + ResponseEditor = 'div.chatMessageContent .interactive-result-editor .inputarea.monaco-mouse-cursor-text', + ResponseMessage = '.chatMessageContent', +} From 6a371bb1fcdd7a935302b594d83903ec627eba7d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:18:46 +0100 Subject: [PATCH 059/141] Fix Activity Bar Icon Clickability in Fullscreen (#207114) fixes #204688 --- src/vs/workbench/browser/parts/media/paneCompositePart.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 455b38249d9..66bc8d9067e 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -66,7 +66,7 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { - height: 24px; + height: 35px; /* matches height of composite container */ padding: 0 5px; } From d6fb91c889af15e5c9eece516ab520798da33c66 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:27:22 -0800 Subject: [PATCH 060/141] show contributed issues in quick access (#206303) * show contributed issues in quick accesss * moved some files around * cleanup unused import * quick access overhaul: add all extensions, filter no duplicates, contributed at bottom, add button to open extension page, add experimental setting * fix to turn on by default * switch to info button icon instad * added issue file on product and marketplace * differentiate btween insiders and stable * some fixes for now * address comments * review fixes --- .../issue/issueReporterModel.ts | 1 + .../issue/issueReporterService.ts | 10 +- src/vs/platform/issue/common/issue.ts | 7 + .../browser/extensions.contribution.ts | 5 + .../issue/browser/issue.contribution.ts | 6 +- .../contrib/issue/browser/issueQuickAccess.ts | 146 ++++++++++++++++++ .../issue/common/issue.contribution.ts | 4 +- .../electron-sandbox/issue.contribution.ts | 39 ++++- 8 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts diff --git a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts index 74f993903e1..1541f98c812 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts @@ -29,6 +29,7 @@ export interface IssueReporterData { extensionsDisabled?: boolean; fileOnExtension?: boolean; fileOnMarketplace?: boolean; + fileOnProduct?: boolean; selectedExtension?: IssueReporterExtensionData; actualSearchResults?: ISettingSearchResult[]; query?: string; diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 56623913d7d..052bb8adc84 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -74,6 +74,10 @@ export class IssueReporter extends Disposable { selectedExtension: targetExtension }); + const fileOnMarketplace = configuration.data.issueSource === IssueSource.Marketplace; + const fileOnProduct = configuration.data.issueSource === IssueSource.VSCode; + this.issueReporterModel.update({ fileOnMarketplace, fileOnProduct }); + //TODO: Handle case where extension is not activated const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { @@ -772,13 +776,17 @@ export class IssueReporter extends Disposable { private setSourceOptions(): void { const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; - const { issueType, fileOnExtension, selectedExtension } = this.issueReporterModel.getData(); + const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData(); let selected = sourceSelect.selectedIndex; if (selected === -1) { if (fileOnExtension !== undefined) { selected = fileOnExtension ? 2 : 1; } else if (selectedExtension?.isBuiltin) { selected = 1; + } else if (fileOnMarketplace) { + selected = 3; + } else if (fileOnProduct) { + selected = 1; } } diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index d1c4e29bb63..df2a1e27599 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -25,6 +25,12 @@ export const enum IssueType { FeatureRequest } +export enum IssueSource { + VSCode = 'vscode', + Extension = 'extension', + Marketplace = 'marketplace' +} + export interface IssueReporterStyles extends WindowStyles { textLinkColor?: string; textLinkActiveForeground?: string; @@ -65,6 +71,7 @@ export interface IssueReporterData extends WindowData { styles: IssueReporterStyles; enabledExtensions: IssueReporterExtensionData[]; issueType?: IssueType; + issueSource?: IssueSource; extensionId?: string; experiments?: string; restrictedMode: boolean; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 571b1041e79..6d09f44869a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -256,6 +256,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsDeferredStartupFinishedActivation', "When enabled, extensions which declare the `onStartupFinished` activation event will be activated after a timeout."), default: false + }, + 'extensions.experimental.issueQuickAccess': { + type: 'boolean', + description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), + default: true } } }); diff --git a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts index 7d19d1cd3c6..28751d1c2c8 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts @@ -13,10 +13,12 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { WebIssueService } from 'vs/workbench/services/issue/browser/issueService'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + class WebIssueContribution extends BaseIssueContribution { - constructor(@IProductService productService: IProductService) { - super(productService); + constructor(@IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService) { + super(productService, configurationService); } } diff --git a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts new file mode 100644 index 00000000000..baa6325a1ae --- /dev/null +++ b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PickerQuickAccessProvider, IPickerQuickAccessItem, FastAndSlowPicks, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Codicon } from 'vs/base/common/codicons'; +import { IssueSource } from 'vs/platform/issue/common/issue'; +import { IProductService } from 'vs/platform/product/common/productService'; + +export class IssueQuickAccess extends PickerQuickAccessProvider { + + static PREFIX = 'issue '; + + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ICommandService private readonly commandService: ICommandService, + @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService + ) { + super(IssueQuickAccess.PREFIX, { canAcceptInBackground: true }); + } + + protected override _getPicks(filter: string): Picks | FastAndSlowPicks | Promise | FastAndSlowPicks> | null { + const issuePicks = new Array(); + const extensionIdSet = new Set(); + + // add regular open issue reporter button + const productLabel = this.productService.nameLong; + issuePicks.push({ + label: productLabel, + ariaLabel: productLabel, + accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.VSCode }) + }); + + issuePicks.push({ type: 'separator' }); + + const marketPlaceLabel = localize("workbench.action.openIssueReporter2", "Extension Marketplace"); + issuePicks.push({ + label: marketPlaceLabel, + ariaLabel: marketPlaceLabel, + accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.Marketplace }) + }); + + issuePicks.push({ type: 'separator', label: localize('extensions', "Extensions: Custom Reporting") }); + + // creates menu from contributed + const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService); + + // render menu and dispose + const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]); + + // create picks from contributed menu + actions.forEach(action => { + if ('source' in action.item && action.item.source) { + extensionIdSet.add(action.item.source.id); + } + + const pick = this._createPick(filter, action); + if (pick) { + issuePicks.push(pick); + } + }); + + menu.dispose(); + + issuePicks.push({ type: 'separator', label: localize('otherExtensions', "Other Extensions") }); + + // create picks from extensions + this.extensionService.extensions.forEach(extension => { + if (!extension.isBuiltin) { + const pick = this._createPick(filter, undefined, extension); + const id = extension.identifier.value; + if (pick) { + if (extensionIdSet.has(id)) { + return; + } + else { + issuePicks.push(pick); + } + } + extensionIdSet.add(id); + } + }); + + return issuePicks; + } + + private _createPick(filter: string, action?: MenuItemAction | SubmenuItemAction | undefined, extension?: IRelaxedExtensionDescription): IPickerQuickAccessItem | undefined { + if (action && 'source' in action.item && action.item.source) { + const label = action.item.source?.title; + const highlights = matchesFuzzy(filter, label, true); + if (highlights) { + return { + label, + highlights: { label: highlights }, + buttons: [{ + iconClass: ThemeIcon.asClassName(Codicon.info), + tooltip: localize('contributedIssuePage', "Open Extension Page") + }], + trigger: () => { + if ('source' in action.item && action.item.source) { + this.commandService.executeCommand('extension.open', action.item.source.id); + } + return TriggerAction.CLOSE_PICKER; + }, + accept: (keyMod, event) => { + action.run(); + } + }; + } + } else if (extension) { + const label = extension.displayName ?? extension.name; + const highlights = matchesFuzzy(filter, label, true); + if (highlights) { + return { + label: label, + highlights: { label: highlights }, + buttons: [{ + iconClass: ThemeIcon.asClassName(Codicon.info), + tooltip: localize('contributedIssuePage', "Open Extension Page") + }], + trigger: () => { + this.commandService.executeCommand('extension.open', extension.identifier.value); + return TriggerAction.CLOSE_PICKER; + }, + accept: (keyMod, event) => { + this.commandService.executeCommand('workbench.action.openIssueReporter', extension.identifier.value); + } + + }; + } + } + return undefined; + } +} diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index 05518d0f4aa..2a48ce274b0 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -12,6 +12,7 @@ import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; const OpenIssueReporterApiId = 'vscode.openIssueReporter'; @@ -59,7 +60,8 @@ interface OpenIssueReporterArgs { export class BaseIssueContribution implements IWorkbenchContribution { constructor( - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, ) { if (!productService.reportIssueUrl) { return; diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 05bc0632624..76ddd71c146 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -19,19 +19,53 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INativeHostService } from 'vs/platform/native/common/native'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IIssueMainService, IssueType } from 'vs/platform/issue/common/issue'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { IssueQuickAccess } from 'vs/workbench/contrib/issue/browser/issueQuickAccess'; + //#region Issue Contribution class NativeIssueContribution extends BaseIssueContribution { constructor( - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - super(productService); + super(productService, configurationService); if (productService.reportIssueUrl) { registerAction2(ReportPerformanceIssueUsingReporterAction); } + + let disposable: IDisposable | undefined; + + const registerQuickAccessProvider = () => { + disposable = Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: IssueQuickAccess, + prefix: IssueQuickAccess.PREFIX, + contextKey: 'inReportIssuePicker', + placeholder: localize('tasksQuickAccessPlaceholder', "Type the name of an extension to report on."), + helpEntries: [{ + description: localize('openIssueReporter', "Open Issue Reporter"), + commandId: 'workbench.action.openIssueReporter' + }] + }); + }; + + configurationService.onDidChangeConfiguration(e => { + if (!configurationService.getValue('extensions.experimental.issueQuickAccess') && disposable) { + disposable.dispose(); + disposable = undefined; + } else if (!disposable) { + registerQuickAccessProvider(); + } + }); + + if (configurationService.getValue('extensions.experimental.issueQuickAccess')) { + registerQuickAccessProvider(); + } } } Registry.as(Extensions.Workbench).registerWorkbenchContribution(NativeIssueContribution, LifecyclePhase.Restored); @@ -133,5 +167,4 @@ registerAction2(StopTracing); CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { return accessor.get(IIssueMainService).getSystemStatus(); }); - //#endregion From 091987d9431154a3624ba20bc1d6de65d71d6472 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 7 Mar 2024 16:04:36 -0800 Subject: [PATCH 061/141] Update paste and drop proposals (#206496) * Update paste and drop proposals Reworks the document paste and drop API proposals. Main highlights: - Align more with code action api - Allow a single paste provider to return multiple edits - Allow resolving applied edits lazily - Switch from using ids to scoped kinds like used for code actions * Adding paste context * Add context * Update test --- extensions/ipynb/src/notebookImagePaste.ts | 15 ++- .../copyFiles/dropOrPasteResource.ts | 22 ++-- .../copyFiles/pasteUrlProvider.ts | 11 +- .../singlefolder-tests/documentPaste.test.ts | 18 +-- src/vs/base/common/hierarchicalKind.ts | 28 ++++ src/vs/base/common/jsonSchema.ts | 19 +++ src/vs/editor/common/languages.ts | 41 ++++-- .../browser/copyPasteContribution.ts | 41 +++--- .../browser/copyPasteController.ts | 108 ++++++++++----- .../browser/defaultProviders.ts | 58 +++++---- .../browser/dropIntoEditorController.ts | 18 +-- .../contrib/dropOrPasteInto/browser/edit.ts | 21 ++- .../dropOrPasteInto/browser/postEditWidget.ts | 34 +++-- .../test/browser/editSort.test.ts | 40 +++--- .../api/browser/mainThreadLanguageFeatures.ts | 65 +++++---- .../workbench/api/common/extHost.api.impl.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 27 +++- .../api/common/extHostLanguageFeatures.ts | 95 +++++++++----- src/vs/workbench/api/common/extHostTypes.ts | 44 ++++++- .../vscode.proposed.documentPaste.d.ts | 123 ++++++++++++++---- .../vscode.proposed.dropMetadata.d.ts | 33 +++-- 21 files changed, 585 insertions(+), 278 deletions(-) create mode 100644 src/vs/base/common/hierarchicalKind.ts diff --git a/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index 94292c26a74..263a610da85 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/extensions/ipynb/src/notebookImagePaste.ts @@ -48,14 +48,15 @@ function getImageMimeType(uri: vscode.Uri): string | undefined { class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public readonly id = 'insertAttachment'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'); async provideDocumentPasteEdits( document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { + ): Promise { const enabled = vscode.workspace.getConfiguration('ipynb', document).get('pasteImagesAsAttachments.enabled', true); if (!enabled) { return; @@ -66,10 +67,10 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod return; } - const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment')); + const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'), DropOrPasteEditProvider.kind); pasteEdit.yieldTo = [{ mimeType: MimeType.plain }]; pasteEdit.additionalEdit = insert.additionalEdit; - return pasteEdit; + return [pasteEdit]; } async provideDocumentDropEdits( @@ -86,7 +87,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod const dropEdit = new vscode.DocumentDropEdit(insert.insertText); dropEdit.yieldTo = [{ mimeType: MimeType.plain }]; dropEdit.additionalEdit = insert.additionalEdit; - dropEdit.label = vscode.l10n.t('Insert Image as Attachment'); + dropEdit.title = vscode.l10n.t('Insert Image as Attachment'); return dropEdit; } @@ -299,14 +300,14 @@ export function notebookImagePasteSetup(): vscode.Disposable { const provider = new DropOrPasteEditProvider(); return vscode.Disposable.from( vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, { - id: provider.id, + providedPasteEditKinds: [DropOrPasteEditProvider.kind], pasteMimeTypes: [ MimeType.png, MimeType.uriList, ], }), vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, { - id: provider.id, + providedDropEditKinds: [DropOrPasteEditProvider.kind], dropMimeTypes: [ ...Object.values(imageExtToMime), MimeType.uriList, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 41c8bd13146..20ea5a17743 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -22,7 +22,7 @@ import { createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from ' */ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public static readonly id = 'insertResource'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); public static readonly mimeTypes = [ Mime.textUriList, @@ -32,7 +32,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v private readonly _yieldTo = [ { mimeType: 'text/plain' }, - { extensionId: 'vscode.ipynb', providerId: 'insertAttachment' }, + { kind: vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment') }, ]; public async provideDocumentDropEdits( @@ -62,8 +62,9 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { + ): Promise { const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.filePaste.enabled', true); if (!enabled) { return; @@ -71,14 +72,15 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v const createEdit = await this._getMediaFilesPasteEdit(document, dataTransfer, token); if (createEdit) { - return createEdit; + return [createEdit]; } if (token.isCancellationRequested) { return; } - return this._createEditFromUriListData(document, ranges, dataTransfer, token); + const edit = await this._createEditFromUriListData(document, ranges, dataTransfer, token); + return edit ? [edit] : undefined; } private async _createEditFromUriListData( @@ -97,7 +99,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - const uriEdit = new vscode.DocumentPasteEdit('', pasteEdit.label); + const uriEdit = new vscode.DocumentPasteEdit('', pasteEdit.label, ResourcePasteOrDropProvider.kind); const edit = new vscode.WorkspaceEdit(); edit.set(document.uri, pasteEdit.edits); uriEdit.additionalEdit = edit; @@ -124,7 +126,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label); + const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, ResourcePasteOrDropProvider.kind); pasteEdit.additionalEdit = edit.additionalEdits; pasteEdit.yieldTo = this._yieldTo; return pasteEdit; @@ -150,7 +152,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v } const dropEdit = new vscode.DocumentDropEdit(edit.snippet); - dropEdit.label = edit.label; + dropEdit.title = edit.label; dropEdit.additionalEdit = edit.additionalEdits; dropEdit.yieldTo = this._yieldTo; return dropEdit; @@ -226,11 +228,11 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector): vscode.Disposable { return vscode.Disposable.from( vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(), { - id: ResourcePasteOrDropProvider.id, + providedPasteEditKinds: [ResourcePasteOrDropProvider.kind], pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes, }), vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(), { - id: ResourcePasteOrDropProvider.id, + providedDropEditKinds: [ResourcePasteOrDropProvider.kind], dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes, }), ); diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index a57f0d39005..193112e9630 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -29,7 +29,7 @@ function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): Paste */ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { - public static readonly id = 'insertMarkdownLink'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); public static readonly pasteMimeTypes = [Mime.textPlain]; @@ -41,8 +41,9 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { + ): Promise { const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document); if (pasteUrlSetting === PasteUrlAsMarkdownLink.Never) { return; @@ -64,7 +65,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { return; } - const pasteEdit = new vscode.DocumentPasteEdit('', edit.label); + const pasteEdit = new vscode.DocumentPasteEdit('', edit.label, PasteUrlEditProvider.kind); const workspaceEdit = new vscode.WorkspaceEdit(); workspaceEdit.set(document.uri, edit.edits); pasteEdit.additionalEdit = workspaceEdit; @@ -73,13 +74,13 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { pasteEdit.yieldTo = [{ mimeType: Mime.textPlain }]; } - return pasteEdit; + return [pasteEdit]; } } export function registerPasteUrlSupport(selector: vscode.DocumentSelector, parser: IMdParser) { return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteUrlEditProvider(parser), { - id: PasteUrlEditProvider.id, + providedPasteEditKinds: [PasteUrlEditProvider.kind], pasteMimeTypes: PasteUrlEditProvider.pasteMimeTypes, }); } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts index c2cdd073d74..e2145d4ee28 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts @@ -37,7 +37,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed)); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -62,7 +62,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed + '\n')); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -88,7 +88,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(`(${ranges.length})${selections.join(' ')}`)); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); editor.selections = [new vscode.Selection(0, 0, 0, 0)]; @@ -118,7 +118,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('a')); providerAResolve(); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); // Later registered providers will be called first testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { @@ -132,7 +132,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('b')); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -159,7 +159,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); providerAResolve(); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { @@ -172,7 +172,7 @@ suite.skip('vscode API - Copy Paste', function () { const str = await entry!.asString(); dataTransfer.set(textPlain, new vscode.DataTransferItem(reverseString(str))); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -192,13 +192,13 @@ suite.skip('vscode API - Copy Paste', function () { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], _dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { throw new Error('Expected testing error from bad provider'); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); diff --git a/src/vs/base/common/hierarchicalKind.ts b/src/vs/base/common/hierarchicalKind.ts new file mode 100644 index 00000000000..4df722d8b99 --- /dev/null +++ b/src/vs/base/common/hierarchicalKind.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class HierarchicalKind { + public static readonly sep = '.'; + + constructor( + public readonly value: string + ) { } + + public equals(other: HierarchicalKind): boolean { + return this.value === other.value; + } + + public contains(other: HierarchicalKind): boolean { + return this.equals(other) || this.value === '' || other.value.startsWith(this.value + HierarchicalKind.sep); + } + + public intersects(other: HierarchicalKind): boolean { + return this.contains(other) || other.contains(this); + } + + public append(...parts: string[]): HierarchicalKind { + return new HierarchicalKind((this.value ? [this.value, ...parts] : parts).join(HierarchicalKind.sep)); + } +} diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 81262c2f46a..4216b0e5c0d 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -99,3 +99,22 @@ export interface IJSONSchemaSnippet { body?: any; // a object that will be JSON stringified bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t) } + +/** + * Converts a basic JSON schema to a TypeScript type. + * + * TODO: only supports basic schemas. Doesn't support all JSON schema features. + */ +export type SchemaToType = T extends { type: 'string' } + ? string + : T extends { type: 'number' } + ? number + : T extends { type: 'boolean' } + ? boolean + : T extends { type: 'null' } + ? null + : T extends { type: 'object'; properties: infer P } + ? { [K in keyof P]: SchemaToType } + : T extends { type: 'array'; items: infer I } + ? Array> + : never; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 5af870d82ff..8f8709f5d05 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -25,6 +25,7 @@ import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import { LanguageFilter } from 'vs/editor/common/languageSelector'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; /** * @internal @@ -821,35 +822,52 @@ export interface CodeActionProvider { * @internal */ export interface DocumentPasteEdit { - readonly label: string; - readonly detail: string; + readonly title: string; + readonly kind: HierarchicalKind; readonly handledMimeType?: string; readonly yieldTo?: readonly DropYieldTo[]; insertText: string | { readonly snippet: string }; additionalEdit?: WorkspaceEdit; } +/** + * @internal + */ +export enum DocumentPasteTriggerKind { + Automatic = 0, + PasteAs = 1, +} + /** * @internal */ export interface DocumentPasteContext { - readonly only?: string; - readonly trigger: 'explicit' | 'implicit'; + readonly only?: HierarchicalKind; + readonly triggerKind: DocumentPasteTriggerKind; +} + +/** + * @internal + */ +export interface DocumentPasteEditsSession { + edits: readonly DocumentPasteEdit[]; + dispose(): void; } /** * @internal */ export interface DocumentPasteEditProvider { - - readonly id: string; - + readonly id?: string; readonly copyMimeTypes?: readonly string[]; readonly pasteMimeTypes?: readonly string[]; + readonly providedPasteEditKinds?: readonly HierarchicalKind[]; prepareDocumentPaste?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; - provideDocumentPasteEdits?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise; + provideDocumentPasteEdits?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise; + + resolveDocumentPasteEdit?(edit: DocumentPasteEdit, token: CancellationToken): Promise; } /** @@ -2114,13 +2132,14 @@ export enum ExternalUriOpenerPriority { /** * @internal */ -export type DropYieldTo = { readonly providerId: string } | { readonly mimeType: string }; +export type DropYieldTo = { readonly kind: string } | { readonly mimeType: string }; /** * @internal */ export interface DocumentOnDropEdit { - readonly label: string; + readonly title: string; + readonly kind: HierarchicalKind | undefined; readonly handledMimeType?: string; readonly yieldTo?: readonly DropYieldTo[]; insertText: string | { readonly snippet: string }; @@ -2134,7 +2153,7 @@ export interface DocumentOnDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; } export interface DocumentContextItem { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts index fbdd84d88d4..c291012f89f 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; +import { IJSONSchema, SchemaToType } from 'vs/base/common/jsonSchema'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -34,7 +36,19 @@ registerEditorCommand(new class extends EditorCommand { } }); -registerEditorAction(class extends EditorAction { + + +registerEditorAction(class PasteAsAction extends EditorAction { + private static readonly argsSchema = { + type: 'object', + properties: { + kind: { + type: 'string', + description: nls.localize('pasteAs.kind', "The kind of the paste edit to try applying. If not provided or there are multiple edits for this kind, the editor will show a picker."), + } + }, + } as const satisfies IJSONSchema; + constructor() { super({ id: 'editor.action.pasteAs', @@ -45,23 +59,20 @@ registerEditorAction(class extends EditorAction { description: 'Paste as', args: [{ name: 'args', - schema: { - type: 'object', - properties: { - 'id': { - type: 'string', - description: nls.localize('pasteAs.id', "The id of the paste edit to try applying. If not provided, the editor will show a picker."), - } - }, - } + schema: PasteAsAction.argsSchema }] } }); } - public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) { - const id = typeof args?.id === 'string' ? args.id : undefined; - return CopyPasteController.get(editor)?.pasteAs(id); + public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args?: SchemaToType) { + let kind = typeof args?.kind === 'string' ? args.kind : undefined; + if (!kind && args) { + // Support old id property + // TODO: remove this in the future + kind = typeof (args as any).id === 'string' ? (args as any).id : undefined; + } + return CopyPasteController.get(editor)?.pasteAs(kind ? { kind: new HierarchicalKind(kind) } : undefined); } }); @@ -75,7 +86,7 @@ registerEditorAction(class extends EditorAction { }); } - public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) { - return CopyPasteController.get(editor)?.pasteAs('text'); + public override run(_accessor: ServicesAccessor, editor: ICodeEditor) { + return CopyPasteController.get(editor)?.pasteAs({ providerId: 'text' }); } }); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index adc0684cfca..783cd613daa 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -20,7 +20,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon'; -import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages'; +import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; @@ -34,6 +34,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { PostEditWidgetManager } from './postEditWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; export const changePasteTypeCommandId = 'editor.changePasteType'; @@ -48,6 +49,14 @@ interface CopyMetadata { readonly defaultPastePayload: Omit; } +type PasteEditWithProvider = DocumentPasteEdit & { + provider: DocumentPasteEditProvider; +}; + +type PastePreference = + | { kind: HierarchicalKind } + | { providerId: string }; + export class CopyPasteController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.copyPasteActionController'; @@ -71,10 +80,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi private readonly _editor: ICodeEditor; private _currentPasteOperation?: CancelablePromise; - private _pasteAsActionContext?: { readonly preferredId: string | undefined }; + private _pasteAsActionContext?: { readonly preferred?: PastePreference }; private readonly _pasteProgressManager: InlineProgressManager; - private readonly _postPasteWidgetManager: PostEditWidgetManager; + private readonly _postPasteWidgetManager: PostEditWidgetManager; constructor( editor: ICodeEditor, @@ -103,10 +112,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._postPasteWidgetManager.tryShowSelector(); } - public pasteAs(preferredId?: string) { + public pasteAs(preferred?: { kind: HierarchicalKind } | { providerId: string }) { this._editor.focus(); try { - this._pasteAsActionContext = { preferredId }; + this._pasteAsActionContext = { preferred: preferred }; getActiveDocument().execCommand('paste'); } finally { this._pasteAsActionContext = undefined; @@ -253,17 +262,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi const allProviders = this._languageFeaturesService.documentPasteEditProvider .ordered(model) .filter(provider => { - if (this._pasteAsActionContext?.preferredId) { - if (this._pasteAsActionContext.preferredId !== provider.id) { + // Filter out providers that don't match the requested paste types + const preference = this._pasteAsActionContext?.preferred; + if (preference) { + if (provider.providedPasteEditKinds && !matchesPreference(provider, preference)) { return false; } } + // And providers that don't handle any of mime types in the clipboard return provider.pasteMimeTypes?.some(type => matchesMimeType(type, allPotentialMimeTypes)); }); if (!allProviders.length) { - if (this._pasteAsActionContext?.preferredId) { - this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext?.preferredId); + if (this._pasteAsActionContext?.preferred) { + this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred); } return; } @@ -275,17 +287,17 @@ export class CopyPasteController extends Disposable implements IEditorContributi e.stopImmediatePropagation(); if (this._pasteAsActionContext) { - this.showPasteAsPick(this._pasteAsActionContext.preferredId, allProviders, selections, dataTransfer, metadata, { trigger: 'explicit', only: this._pasteAsActionContext.preferredId }); + this.showPasteAsPick(this._pasteAsActionContext.preferred, allProviders, selections, dataTransfer, metadata); } else { - this.doPasteInline(allProviders, selections, dataTransfer, metadata, { trigger: 'implicit' }); + this.doPasteInline(allProviders, selections, dataTransfer, metadata); } } - private showPasteAsNoEditMessage(selections: readonly Selection[], editId: string) { - MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", editId), selections[0].getStartPosition()); + private showPasteAsNoEditMessage(selections: readonly Selection[], preference: PastePreference) { + MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", 'kind' in preference ? preference.kind.value : preference.providerId), selections[0].getStartPosition()); } - private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, context: DocumentPasteContext): void { + private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void { const p = createCancelablePromise(async (token) => { const editor = this._editor; if (!editor.hasModel()) { @@ -301,6 +313,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi } // Filter out any providers the don't match the full data transfer we will send them. + // TODO: also filter based on kinds const supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer)); if (!supportedProviders.length || (supportedProviders.length === 1 && supportedProviders[0].id === 'text') // Only our default text provider is active @@ -309,20 +322,29 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } + const context = { + triggerKind: DocumentPasteTriggerKind.Automatic, + }; const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } // If the only edit returned is a text edit, use the default paste handler - if (providerEdits.length === 1 && providerEdits[0].providerId === 'text') { + if (providerEdits.length === 1 && providerEdits[0].kind.value === 'text') { await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token); return; } if (providerEdits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, tokenSource.token); + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, async (edit, token) => { + const resolved = await edit.provider.resolveDocumentPasteEdit?.(edit, token); + if (resolved) { + edit.additionalEdit = resolved.additionalEdit; + } + return edit; + }, tokenSource.token); } await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token); @@ -338,7 +360,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._currentPasteOperation = p; } - private showPasteAsPick(preferredId: string | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, context: DocumentPasteContext): void { + private showPasteAsPick(preference: PastePreference | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void { const p = createCancelablePromise(async (token) => { const editor = this._editor; if (!editor.hasModel()) { @@ -355,32 +377,46 @@ export class CopyPasteController extends Disposable implements IEditorContributi // Filter out any providers the don't match the full data transfer we will send them. let supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer)); - if (preferredId) { + if (preference) { // We are looking for a specific edit - supportedProviders = supportedProviders.filter(edit => edit.id === preferredId); + supportedProviders = supportedProviders.filter(provider => matchesPreference(provider, preference)); } - const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); + const context = { + triggerKind: DocumentPasteTriggerKind.PasteAs, + only: preference && 'kind' in preference ? preference.kind : undefined, + }; + let providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } + // Filter out any edits that don't match the requested kind + if (preference) { + providerEdits = providerEdits.filter(edit => { + if ('kind' in preference) { + return preference.kind.contains(edit.kind); + } else { + return preference.providerId === edit.provider.id; + } + }); + } + if (!providerEdits.length) { if (context.only) { - this.showPasteAsNoEditMessage(selections, context.only); + this.showPasteAsNoEditMessage(selections, { kind: context.only }); } return; } let pickedEdit: DocumentPasteEdit | undefined; - if (preferredId) { + if (preference) { pickedEdit = providerEdits.at(0); } else { const selected = await this._quickInputService.pick( providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ - label: edit.label, - description: edit.providerId, - detail: edit.detail, + label: edit.title, + description: edit.kind?.value, edit, })), { placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"), @@ -466,21 +502,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise> { + private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { const results = await raceCancellation( Promise.all(providers.map(async provider => { try { - const edit = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); - if (edit) { - return { ...edit, providerId: provider.id }; - } + const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); + // TODO: dispose of edits + return edits?.edits?.map(edit => ({ ...edit, provider })); } catch (err) { console.error(err); } return undefined; })), token); - const edits = coalesce(results ?? []); + const edits = coalesce(results ?? []).flat(); return sortEditsByYieldTo(edits); } @@ -508,3 +543,14 @@ export class CopyPasteController extends Disposable implements IEditorContributi function isSupportedPasteProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer): boolean { return Boolean(provider.pasteMimeTypes?.some(type => dataTransfer.matches(type))); } + +function matchesPreference(provider: DocumentPasteEditProvider, preference: PastePreference): boolean { + if ('kind' in preference) { + if (!provider.providedPasteEditKinds) { + return true; + } + return provider.providedPasteEditKinds.some(providedKind => preference.kind.contains(providedKind)); + } else { + return provider.id === preference.providerId; + } +} diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 27812236c50..ca2d17065c3 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IReadonlyVSDataTransfer, UriList } from 'vs/base/common/dataTransfer'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; @@ -13,28 +14,34 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentOnDropEdit, DocumentOnDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentOnDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -const builtInLabel = localize('builtIn', 'Built-in'); abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, DocumentPasteEditProvider { - abstract readonly id: string; + abstract readonly kind: string; abstract readonly dropMimeTypes: readonly string[] | undefined; abstract readonly pasteMimeTypes: readonly string[]; - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? { insertText: edit.insertText, label: edit.label, detail: edit.detail, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined; + if (!edit) { + return undefined; + } + + return { + dispose() { }, + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] + }; } - async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? { insertText: edit.insertText, label: edit.label, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined; + return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; @@ -42,7 +49,7 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, class DefaultTextProvider extends SimplePasteAndDropProvider { - readonly id = 'text'; + readonly kind = 'text'; readonly dropMimeTypes = [Mimes.text]; readonly pasteMimeTypes = [Mimes.text]; @@ -61,16 +68,16 @@ class DefaultTextProvider extends SimplePasteAndDropProvider { const insertText = await textEntry.asString(); return { handledMimeType: Mimes.text, - label: localize('text.label', "Insert Plain Text"), - detail: builtInLabel, - insertText + title: localize('text.label', "Insert Plain Text"), + insertText, + kind: new HierarchicalKind(this.kind), }; } } class PathProvider extends SimplePasteAndDropProvider { - readonly id = 'uri'; + readonly kind = 'uri'; readonly dropMimeTypes = [Mimes.uriList]; readonly pasteMimeTypes = [Mimes.uriList]; @@ -108,15 +115,15 @@ class PathProvider extends SimplePasteAndDropProvider { return { handledMimeType: Mimes.uriList, insertText, - label, - detail: builtInLabel, + title: label, + kind: new HierarchicalKind(this.kind), }; } } class RelativePathProvider extends SimplePasteAndDropProvider { - readonly id = 'relativePath'; + readonly kind = 'relativePath'; readonly dropMimeTypes = [Mimes.uriList]; readonly pasteMimeTypes = [Mimes.uriList]; @@ -144,24 +151,24 @@ class RelativePathProvider extends SimplePasteAndDropProvider { return { handledMimeType: Mimes.uriList, insertText: relativeUris.join(' '), - label: entries.length > 1 + title: entries.length > 1 ? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths") : localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path"), - detail: builtInLabel, + kind: new HierarchicalKind(this.kind), }; } } class PasteHtmlProvider implements DocumentPasteEditProvider { - public readonly id = 'html'; + public readonly kind = new HierarchicalKind('html'); public readonly pasteMimeTypes = ['text/html']; private readonly _yieldTo = [{ mimeType: Mimes.text }]; - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { - if (context.trigger !== 'explicit' && context.only !== this.id) { + async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + if (context.triggerKind !== DocumentPasteTriggerKind.PasteAs && context.only?.contains(this.kind)) { return; } @@ -172,10 +179,13 @@ class PasteHtmlProvider implements DocumentPasteEditProvider { } return { - insertText: htmlText, - yieldTo: this._yieldTo, - label: localize('pasteHtmlLabel', 'Insert HTML'), - detail: builtInLabel, + dispose() { }, + edits: [{ + insertText: htmlText, + yieldTo: this._yieldTo, + title: localize('pasteHtmlLabel', 'Insert HTML'), + kind: this.kind, + }], }; } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 48dae75565c..32e64cff650 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -45,7 +46,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr private _currentOperation?: CancelablePromise; private readonly _dropProgressManager: InlineProgressManager; - private readonly _postDropWidgetManager: PostEditWidgetManager; + private readonly _postDropWidgetManager: PostEditWidgetManager; private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance(); @@ -115,7 +116,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr const activeEditIndex = this.getInitialActiveEditIndex(model, edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); } } finally { tokenSource.dispose(); @@ -132,25 +133,24 @@ export class DropIntoEditorController extends Disposable implements IEditorContr private async getDropEdits(providers: readonly DocumentOnDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { const results = await raceCancellation(Promise.all(providers.map(async provider => { try { - const edit = await provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token); - if (edit) { - return { ...edit, providerId: provider.id }; - } + const edits = await provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token); + return edits?.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } return undefined; })), tokenSource.token); - const edits = coalesce(results ?? []); + const edits = coalesce(results ?? []).flat(); return sortEditsByYieldTo(edits); } private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { const preferredProviders = this._configService.getValue>(defaultProviderConfig, { resource: model.uri }); - for (const [configMime, desiredId] of Object.entries(preferredProviders)) { + for (const [configMime, desiredKindStr] of Object.entries(preferredProviders)) { + const desiredKind = new HierarchicalKind(desiredKindStr); const editIndex = edits.findIndex(edit => - desiredId === edit.providerId + desiredKind.value === edit.providerId && edit.handledMimeType && matchesMimeType(configMime, [edit.handledMimeType])); if (editIndex >= 0) { return editIndex; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts index 55d8dff9e7b..252c1863cba 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts @@ -5,21 +5,16 @@ import { URI } from 'vs/base/common/uri'; import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentPasteEdit, DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; import { Range } from 'vs/editor/common/core/range'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; - -export interface DropOrPasteEdit { - readonly label: string; - readonly insertText: string | { readonly snippet: string }; - readonly additionalEdit?: WorkspaceEdit; -} +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; /** * Given a {@link DropOrPasteEdit} and set of ranges, creates a {@link WorkspaceEdit} that applies the insert text from * the {@link DropOrPasteEdit} at each range plus any additional edits. */ -export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DropOrPasteEdit): WorkspaceEdit { +export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DocumentPasteEdit | DocumentOnDropEdit): WorkspaceEdit { // If the edit insert text is empty, skip applying at each range if (typeof edit.insertText === 'string' ? edit.insertText === '' : edit.insertText.snippet === '') { return { @@ -39,13 +34,15 @@ export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], } export function sortEditsByYieldTo(edits: readonly T[]): T[] { function yieldsTo(yTo: DropYieldTo, other: T): boolean { - return ('providerId' in yTo && yTo.providerId === other.providerId) - || ('mimeType' in yTo && yTo.mimeType === other.handledMimeType); + if ('mimeType' in yTo) { + return yTo.mimeType === other.handledMimeType; + } + return !!other.kind && other.kind.contains(new HierarchicalKind(yTo.kind)); } // Build list of nodes each node yields to @@ -84,7 +81,7 @@ export function sortEditsByYieldTo { readonly activeEditIndex: number; - readonly allEdits: ReadonlyArray<{ - readonly label: string; - readonly insertText: string | { readonly snippet: string }; - readonly additionalEdit?: WorkspaceEdit; - }>; + readonly allEdits: ReadonlyArray; } interface ShowCommand { @@ -36,7 +32,7 @@ interface ShowCommand { readonly label: string; } -class PostEditWidget extends Disposable implements IContentWidget { +class PostEditWidget extends Disposable implements IContentWidget { private static readonly baseId = 'editor.widget.postEditWidget'; readonly allowEditorOverflow = true; @@ -53,7 +49,7 @@ class PostEditWidget extends Disposable implements IContentWidget { visibleContext: RawContextKey, private readonly showCommand: ShowCommand, private readonly range: Range, - private readonly edits: EditSet, + private readonly edits: EditSet, private readonly onSelectNewEdit: (editIndex: number) => void, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService, @@ -123,7 +119,7 @@ class PostEditWidget extends Disposable implements IContentWidget { getActions: () => { return this.edits.allEdits.map((edit, i) => toAction({ id: '', - label: edit.label, + label: edit.title, checked: i === this.edits.activeEditIndex, run: () => { if (i !== this.edits.activeEditIndex) { @@ -136,9 +132,9 @@ class PostEditWidget extends Disposable implements IContentWidget { } } -export class PostEditWidgetManager extends Disposable { +export class PostEditWidgetManager extends Disposable { - private readonly _currentWidget = this._register(new MutableDisposable()); + private readonly _currentWidget = this._register(new MutableDisposable>()); constructor( private readonly _id: string, @@ -156,18 +152,20 @@ export class PostEditWidgetManager extends Disposable { )(() => this.clear())); } - public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet, canShowWidget: boolean, token: CancellationToken) { + public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise, token: CancellationToken) { const model = this._editor.getModel(); if (!model || !ranges.length) { return; } - const edit = edits.allEdits[edits.activeEditIndex]; + const edit = edits.allEdits.at(edits.activeEditIndex); if (!edit) { return; } - const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, edit); + const resolvedEdit = await resolve(edit, token); + + const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit); // Use a decoration to track edits around the trigger range const primaryRange = ranges[0]; @@ -193,16 +191,16 @@ export class PostEditWidgetManager extends Disposable { } await model.undo(); - this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, token); + this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token); }); } } - public show(range: Range, edits: EditSet, onDidSelectEdit: (newIndex: number) => void) { + public show(range: Range, edits: EditSet, onDidSelectEdit: (newIndex: number) => void) { this.clear(); if (this._editor.hasModel()) { - this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit); + this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit); } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index f41f6866982..884b7bac151 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DocumentOnDropEdit } from 'vs/editor/common/languages'; import { sortEditsByYieldTo } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; -type DropEdit = DocumentOnDropEdit & { providerId: string | undefined }; -function createTestEdit(providerId: string, args?: Partial): DropEdit { +function createTestEdit(kind: string, args?: Partial): DocumentOnDropEdit { return { - label: '', + title: '', insertText: '', - providerId, + kind: new HierarchicalKind(kind), ...args, }; } @@ -21,48 +21,48 @@ function createTestEdit(providerId: string, args?: Partial): DropEdit suite('sortEditsByYieldTo', () => { test('Should noop for empty edits', () => { - const edits: DropEdit[] = []; + const edits: DocumentOnDropEdit[] = []; assert.deepStrictEqual(sortEditsByYieldTo(edits), []); }); test('Yielded to edit should get sorted after target', () => { - const edits: DropEdit[] = [ - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('a', { yieldTo: [{ kind: 'b' }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a']); }); test('Should handle chain of yield to', () => { { - const edits: DropEdit[] = [ - createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('c', { yieldTo: [{ kind: 'a' }] }), + createTestEdit('a', { yieldTo: [{ kind: 'b' }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a', 'c']); } { - const edits: DropEdit[] = [ - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), - createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('a', { yieldTo: [{ kind: 'b' }] }), + createTestEdit('c', { yieldTo: [{ kind: 'a' }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a', 'c']); } }); test(`Should not reorder when yield to isn't used`, () => { - const edits: DropEdit[] = [ - createTestEdit('c', { yieldTo: [{ providerId: 'x' }] }), - createTestEdit('a', { yieldTo: [{ providerId: 'y' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('c', { yieldTo: [{ kind: 'x' }] }), + createTestEdit('a', { yieldTo: [{ kind: 'y' }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['c', 'a', 'b']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['c', 'a', 'b']); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index b7ecbfdb9a2..a41310b7ce8 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -32,9 +32,10 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; import { ResourceMap } from 'vs/base/common/map'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -398,8 +399,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread private readonly _pasteEditProviders = new Map(); - $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], id: string, metadata: IPasteEditProviderMetadataDto): void { - const provider = new MainThreadPasteEditProvider(handle, this._proxy, id, metadata, this._uriIdentService); + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void { + const provider = new MainThreadPasteEditProvider(handle, this._proxy, metadata, this._uriIdentService); this._pasteEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( this._languageFeaturesService.documentPasteEditProvider.register(selector, provider), @@ -963,8 +964,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread private readonly _documentOnDropEditProviders = new Map(); - $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], id: string | undefined, metadata: IDocumentDropEditProviderMetadata): void { - const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy, id, metadata, this._uriIdentService); + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IDocumentDropEditProviderMetadata): void { + const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy, metadata, this._uriIdentService); this._documentOnDropEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( this._languageFeaturesService.documentOnDropEditProvider.register(selector, provider), @@ -992,23 +993,23 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider private readonly dataTransfers = new DataTransferFileCache(); - public readonly id: string; public readonly copyMimeTypes?: readonly string[]; public readonly pasteMimeTypes?: readonly string[]; + public readonly providedPasteEditKinds?: readonly HierarchicalKind[]; readonly prepareDocumentPaste?: languages.DocumentPasteEditProvider['prepareDocumentPaste']; readonly provideDocumentPasteEdits?: languages.DocumentPasteEditProvider['provideDocumentPasteEdits']; + readonly resolveDocumentPasteEdit?: languages.DocumentPasteEditProvider['resolveDocumentPasteEdit']; constructor( private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, - id: string, metadata: IPasteEditProviderMetadataDto, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { - this.id = id; this.copyMimeTypes = metadata.copyMimeTypes; this.pasteMimeTypes = metadata.pasteMimeTypes; + this.providedPasteEditKinds = metadata.providedPasteEditKinds?.map(kind => new HierarchicalKind(kind)); if (metadata.supportsCopy) { this.prepareDocumentPaste = async (model: ITextModel, selections: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise => { @@ -1039,20 +1040,40 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider return; } - const result = await this._proxy.$providePasteEdits(this._handle, request.id, model.uri, selections, dataTransferDto, token); - if (!result) { + const edits = await this._proxy.$providePasteEdits(this._handle, request.id, model.uri, selections, dataTransferDto, { + only: context.only?.value, + triggerKind: context.triggerKind, + }, token); + if (!edits) { return; } return { - ...result, - additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined, + edits: edits.map((edit): languages.DocumentPasteEdit => { + return { + ...edit, + kind: edit.kind ? new HierarchicalKind(edit.kind.value) : new HierarchicalKind(''), + additionalEdit: edit.additionalEdit ? reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined, + }; + }), + dispose: () => { + this._proxy.$releasePasteEdits(this._handle, request.id); + }, }; } finally { request.dispose(); } }; } + if (metadata.supportsResolve) { + this.resolveDocumentPasteEdit = async (edit: languages.DocumentPasteEdit, token: CancellationToken) => { + const resolved = await this._proxy.$resolvePasteEdit(this._handle, (edit)._cacheId!, token); + if (resolved.additionalEdit) { + edit.additionalEdit = reviveWorkspaceEditDto(resolved.additionalEdit, this._uriIdentService); + } + return edit; + }; + } } resolveFileData(requestId: number, dataId: string): Promise { @@ -1064,21 +1085,18 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd private readonly dataTransfers = new DataTransferFileCache(); - readonly id: string | undefined; readonly dropMimeTypes?: readonly string[]; constructor( private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, - id: string | undefined, metadata: IDocumentDropEditProviderMetadata | undefined, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { - this.id = id; this.dropMimeTypes = metadata?.dropMimeTypes ?? ['*/*']; } - async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1086,15 +1104,18 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd return; } - const edit = await this._proxy.$provideDocumentOnDropEdits(this._handle, request.id, model.uri, position, dataTransferDto, token); - if (!edit) { + const edits = await this._proxy.$provideDocumentOnDropEdits(this._handle, request.id, model.uri, position, dataTransferDto, token); + if (!edits) { return; } - return { - ...edit, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; + return edits.map(edit => { + return { + ...edit, + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }); } finally { request.dispose(); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 20f8ebcdad9..f1a443f47e3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1613,7 +1613,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types + DocumentPasteTriggerKind: extHostTypes.DocumentPasteTriggerKind, DocumentDropEdit: extHostTypes.DocumentDropEdit, + DocumentPasteEditKind: extHostTypes.DocumentPasteEditKind, DocumentPasteEdit: extHostTypes.DocumentPasteEdit, InlayHint: extHostTypes.InlayHint, InlayHintLabelPart: extHostTypes.InlayHintLabelPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e002c997721..9055bfa9aea 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -414,7 +414,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; - $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], id: string, metadata: IPasteEditProviderMetadataDto): void; + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, supportRanges: boolean): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -437,7 +437,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; - $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], id: string | undefined, metadata?: IDocumentDropEditProviderMetadata): void; + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], metadata?: IDocumentDropEditProviderMetadata): void; $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; @@ -2061,13 +2061,23 @@ export type ITypeHierarchyItemDto = Dto; export interface IPasteEditProviderMetadataDto { readonly supportsCopy: boolean; readonly supportsPaste: boolean; + readonly supportsResolve: boolean; + + readonly providedPasteEditKinds?: readonly string[]; readonly copyMimeTypes?: readonly string[]; readonly pasteMimeTypes?: readonly string[]; } +export interface IDocumentPasteContextDto { + readonly only: string | undefined; + readonly triggerKind: languages.DocumentPasteTriggerKind; + +} + export interface IPasteEditDto { - label: string; - detail: string; + _cacheId?: ChainedCacheId; + title: string; + kind: { value: string } | undefined; insertText: string | { snippet: string }; additionalEdit?: IWorkspaceEditDto; yieldTo?: readonly languages.DropYieldTo[]; @@ -2078,7 +2088,8 @@ export interface IDocumentDropEditProviderMetadata { } export interface IDocumentOnDropEditDto { - label: string; + title: string; + kind: string | undefined; insertText: string | { snippet: string }; additionalEdit?: IWorkspaceEditDto; yieldTo?: readonly languages.DropYieldTo[]; @@ -2104,7 +2115,9 @@ export interface ExtHostLanguageFeaturesShape { $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<{ edit?: IWorkspaceEditDto; command?: ICommandDto }>; $releaseCodeActions(handle: number, cacheId: number): void; $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: readonly IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; - $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; + $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, context: IDocumentPasteContextDto, token: CancellationToken): Promise; + $resolvePasteEdit(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: IWorkspaceEditDto }>; + $releasePasteEdits(handle: number, cacheId: number): void; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangesFormattingEdits(handle: number, resource: UriComponents, range: IRange[], options: languages.FormattingOptions, token: CancellationToken): Promise; @@ -2146,7 +2159,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySupertypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 1c532bcc8ee..19f6cd063c4 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, isFalsyOrEmpty, isNonEmptyArray } from 'vs/base/common/arrays'; +import { asArray, coalesce, isFalsyOrEmpty, isNonEmptyArray } from 'vs/base/common/arrays'; import { raceCancellationError } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -32,7 +32,7 @@ import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { CodeActionKind, CompletionList, Disposable, DocumentSymbol, InlineCompletionTriggerKind, InternalDataTransferItem, Location, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType, InlineEditTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { CodeActionKind, CompletionList, Disposable, DocumentPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { Cache } from './cache'; @@ -537,9 +537,7 @@ class CodeActionAdapter { class DocumentPasteEditProvider { - public static toInternalProviderId(extId: string, editId: string): string { - return extId + '.' + editId; - } + private readonly _cache = new Cache('DocumentPasteEdit'); constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, @@ -570,9 +568,9 @@ class DocumentPasteEditProvider { return typeConvert.DataTransfer.from(entries); } - async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, context: extHostProtocol.IDocumentPasteContextDto, token: CancellationToken): Promise { if (!this._provider.provideDocumentPasteEdits) { - return; + return []; } const doc = this._documents.getDocument(resource); @@ -582,20 +580,44 @@ class DocumentPasteEditProvider { return (await this._proxy.$resolvePasteFileData(this._handle, requestId, id)).buffer; }); - const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, token); - if (!edit) { - return; + const edits = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, { + only: context.only ? new DocumentPasteEditKind(context.only) : undefined, + triggerKind: context.triggerKind, + }, token); + if (!edits || token.isCancellationRequested) { + return []; } - return { - label: edit.label ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name), + const cacheId = this._cache.add(edits); + + return edits.map((edit, i) => ({ + _cacheId: [cacheId, i], + title: edit.title ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name), + kind: edit.kind, detail: this._extension.displayName || this._extension.name, yieldTo: edit.yieldTo?.map(yTo => { - return 'mimeType' in yTo ? yTo : { providerId: DocumentPasteEditProvider.toInternalProviderId(yTo.extensionId, yTo.providerId) }; + return 'mimeType' in yTo ? yTo : { kind: yTo.kind.value }; }), insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, - }; + })); + } + + + async resolvePasteEdit(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + const [sessionId, itemId] = id; + const item = this._cache.get(sessionId, itemId); + if (!item || !this._provider.resolveDocumentPasteEdit) { + return {}; // this should not happen... + } + + const resolvedItem = (await this._provider.resolveDocumentPasteEdit(item, token)) ?? item; + const additionalEdit = resolvedItem.additionalEdit ? typeConvert.WorkspaceEdit.from(resolvedItem.additionalEdit, undefined) : undefined; + return { additionalEdit }; + } + + releasePasteEdits(id: number): any { + this._cache.delete(id); } } @@ -1940,10 +1962,6 @@ class TypeHierarchyAdapter { class DocumentOnDropEditAdapter { - public static toInternalProviderId(extId: string, editId: string): string { - return extId + '.' + editId; - } - constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, private readonly _documents: ExtHostDocuments, @@ -1952,25 +1970,30 @@ class DocumentOnDropEditAdapter { private readonly _extension: IExtensionDescription, ) { } - async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { const doc = this._documents.getDocument(uri); const pos = typeConvert.Position.to(position); const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => { return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, id)).buffer; }); - const edit = await this._provider.provideDocumentDropEdits(doc, pos, dataTransfer, token); - if (!edit) { + const edits = await this._provider.provideDocumentDropEdits(doc, pos, dataTransfer, token); + if (!edits) { return undefined; } - return { - label: edit.label ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), + + return asArray(edits).map(edit => ({ + title: edit.title ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), + kind: edit.kind?.value, yieldTo: edit.yieldTo?.map(yTo => { - return 'mimeType' in yTo ? yTo : { providerId: DocumentOnDropEditAdapter.toInternalProviderId(yTo.extensionId, yTo.providerId) }; + if ('mimeType' in yTo) { + return yTo; + } + return { kind: yTo.kind.value }; }), insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, - }; + })); } } @@ -2692,13 +2715,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._documents, provider, handle, extension), extension)); - const id = isProposedApiEnabled(extension, 'dropMetadata') && metadata ? DocumentOnDropEditAdapter.toInternalProviderId(extension.identifier.value, metadata.id) : undefined; - this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), id, isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); + this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); return this._createDisposable(handle); } - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } @@ -2721,10 +2743,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle, extension), extension)); - const internalId = DocumentPasteEditProvider.toInternalProviderId(extension.identifier.value, metadata.id); - this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector, extension), internalId, { + this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector, extension), { supportsCopy: !!provider.prepareDocumentPaste, supportsPaste: !!provider.provideDocumentPasteEdits, + supportsResolve: !!provider.resolveDocumentPasteEdit, + providedPasteEditKinds: metadata.providedPasteEditKinds?.map(x => x.value), copyMimeTypes: metadata.copyMimeTypes, pasteMimeTypes: metadata.pasteMimeTypes, }); @@ -2735,8 +2758,16 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.prepareDocumentPaste(URI.revive(resource), ranges, dataTransfer, token), undefined, token); } - $providePasteEdits(handle: number, requestId: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(requestId, URI.revive(resource), ranges, dataTransferDto, token), undefined, token); + $providePasteEdits(handle: number, requestId: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, context: extHostProtocol.IDocumentPasteContextDto, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(requestId, URI.revive(resource), ranges, dataTransferDto, context, token), undefined, token); + } + + $resolvePasteEdit(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.resolvePasteEdit(id, token), {}, undefined); + } + + $releasePasteEdits(handle: number, cacheId: number): void { + this._withAdapter(handle, DocumentPasteEditProvider, adapter => Promise.resolve(adapter.releasePasteEdits(cacheId)), undefined, undefined); } // --- configuration diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c2cd247e7dd..058582f76ab 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2786,27 +2786,63 @@ export class DataTransfer implements vscode.DataTransfer { @es5ClassCompat export class DocumentDropEdit { + title?: string; + id: string | undefined; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; - constructor(insertText: string | SnippetString) { + kind?: DocumentPasteEditKind; + + constructor(insertText: string | SnippetString, title?: string, kind?: DocumentPasteEditKind) { this.insertText = insertText; + this.title = title; + this.kind = kind; } } +export enum DocumentPasteTriggerKind { + Automatic = 0, + PasteAs = 1, +} + +export class DocumentPasteEditKind { + static Empty: DocumentPasteEditKind; + + private static sep = '.'; + + constructor( + public readonly value: string + ) { } + + public append(...parts: string[]): DocumentPasteEditKind { + return new DocumentPasteEditKind((this.value ? [this.value, ...parts] : parts).join(DocumentPasteEditKind.sep)); + } + + public intersects(other: DocumentPasteEditKind): boolean { + return this.contains(other) || other.contains(this); + } + + public contains(other: DocumentPasteEditKind): boolean { + return this.value === other.value || other.value.startsWith(this.value + DocumentPasteEditKind.sep); + } +} +DocumentPasteEditKind.Empty = new DocumentPasteEditKind(''); + @es5ClassCompat export class DocumentPasteEdit { - label: string; + title: string; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; + kind: DocumentPasteEditKind; - constructor(insertText: string | SnippetString, label: string) { - this.label = label; + constructor(insertText: string | SnippetString, title: string, kind: DocumentPasteEditKind) { + this.title = title; this.insertText = insertText; + this.kind = kind; } } diff --git a/src/vscode-dts/vscode.proposed.documentPaste.d.ts b/src/vscode-dts/vscode.proposed.documentPaste.d.ts index 4b5e49ab330..4abe25ad160 100644 --- a/src/vscode-dts/vscode.proposed.documentPaste.d.ts +++ b/src/vscode-dts/vscode.proposed.documentPaste.d.ts @@ -7,10 +7,38 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/30066/ + /** + * The reason why paste edits were requested. + */ + export enum DocumentPasteTriggerKind { + /** + * Pasting was requested as part of a normal paste operation. + */ + Automatic = 0, + + /** + * Pasting was requested by the user with the 'paste as' command. + */ + PasteAs = 1, + } + + /** + * Additional information about the paste operation. + */ + + export interface DocumentPasteEditContext { + readonly only: DocumentPasteEditKind | undefined; + + /** + * The reason why paste edits were requested. + */ + readonly triggerKind: DocumentPasteTriggerKind; + } + /** * Provider invoked when the user copies and pastes code. */ - interface DocumentPasteEditProvider { + interface DocumentPasteEditProvider { /** * Optional method invoked after the user copies text in a file. @@ -19,44 +47,60 @@ declare module 'vscode' { * a {@link DataTransfer} and is passed back to the provider in {@link provideDocumentPasteEdits}. * * @param document Document where the copy took place. - * @param ranges Ranges being copied in the `document`. + * @param ranges Ranges being copied in `document`. * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for later use in {@link provideDocumentPasteEdits}. + * This object is only valid for the duration of this method. * @param token A cancellation token. + * + * @return Optional thenable that resolves when all changes to the `dataTransfer` are complete. */ prepareDocumentPaste?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; /** * Invoked before the user pastes into a document. * - * In this method, extensions can return a workspace edit that replaces the standard pasting behavior. + * Returned edits can replace the standard pasting behavior. * * @param document Document being pasted into * @param ranges Currently selected ranges in the document. * @param dataTransfer The data transfer associated with the paste. + * @param context Additional context for the paste. * @param token A cancellation token. * - * @return Optional workspace edit that applies the paste. Return undefined to use standard pasting. + * @return Set of potential {@link DocumentPasteEdit edits} that apply the paste. Return `undefined` to use standard pasting. */ - provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, context: DocumentPasteEditContext, token: CancellationToken): ProviderResult; + + /** + * Optional method which fills in the {@linkcode DocumentPasteEdit.additionalEdit} before the edit is applied. + * + * This should be used if generating the `additionalEdit` may take a long time. + * + * @param pasteEdit The {@linkcode DocumentPasteEdit} to resolve. + * @param token A cancellation token. + * + * @returns The resolved paste edit or a thenable that resolves to such. It is OK to return the given + * `item`. When no result is returned, the given `item` will be used. + */ + resolveDocumentPasteEdit?(pasteEdit: T, token: CancellationToken): ProviderResult; } /** - * An operation applied on paste + * An edit applied on paste */ class DocumentPasteEdit { /** * Human readable label that describes the edit. */ - label: string; + title: string; /** - * Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list. + * {@link DocumentPasteEditKind Kind} of the edit. + * + * Used to identify specific types of edits. */ - yieldTo?: ReadonlyArray< - | { readonly extensionId: string; readonly providerId: string } - | { readonly mimeType: string } - >; + kind: DocumentPasteEditKind; /** * The text or snippet to insert at the pasted locations. @@ -69,37 +113,60 @@ declare module 'vscode' { additionalEdit?: WorkspaceEdit; /** - * @param insertText The text or snippet to insert at the pasted locations. - * - * TODO: Reverse args, but this will break existing consumers :( + * List of mime types that this edit handles. */ - constructor(insertText: string | SnippetString, label: string); + handledMimeTypes?: readonly string[]; + + /** + * Controls the ordering of paste edits provided by multiple providers. + * + * If this edit yields to another, it will be shown lower in the list of paste edit. + */ + yieldTo?: ReadonlyArray<{ readonly kind: DocumentPasteEditKind } | { readonly mimeType: string }>; + + /** + * Create a new paste edit. + * + * @param insertText The text or snippet to insert at the pasted locations. + * @param title Human readable label that describes the edit. + * @param kind {@link DocumentPasteEditKind Kind} of the edit. + */ + constructor(insertText: string | SnippetString, title: string, kind: DocumentPasteEditKind); + } + + + /** + * TODO: Share with code action kind? + */ + class DocumentPasteEditKind { + static readonly Empty: DocumentPasteEditKind; + private constructor(value: string); + + readonly value: string; + + append(...parts: string[]): CodeActionKind; + intersects(other: CodeActionKind): boolean; + contains(other: CodeActionKind): boolean; } interface DocumentPasteProviderMetadata { - /** - * Identifies the provider. - * - * This id is used when users configure the default provider for paste. - * - * This id should be unique within the extension but does not need to be unique across extensions. - */ - readonly id: string; + // TODO + readonly providedPasteEditKinds?: readonly DocumentPasteEditKind[]; /** - * Mime types that {@link DocumentPasteEditProvider.prepareDocumentPaste provideDocumentPasteEdits} may add on copy. + * Mime types that {@linkcode DocumentPasteEditProvider.prepareDocumentPaste prepareDocumentPaste} may add on copy. */ readonly copyMimeTypes?: readonly string[]; /** - * Mime types that {@link DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. + * Mime types that {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. * * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. * * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. * - * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@link DataTransfer}. - * Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as + * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@linkcode DataTransfer}. + * Note that {@linkcode DataTransferFile} entries are only created when dropping content from outside the editor, such as * from the operating system. */ readonly pasteMimeTypes?: readonly string[]; diff --git a/src/vscode-dts/vscode.proposed.dropMetadata.d.ts b/src/vscode-dts/vscode.proposed.dropMetadata.d.ts index b78790d85a6..c851f13f9df 100644 --- a/src/vscode-dts/vscode.proposed.dropMetadata.d.ts +++ b/src/vscode-dts/vscode.proposed.dropMetadata.d.ts @@ -7,11 +7,27 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/179430 + + /** + * TODO: + * - Add ctor(insertText: string | SnippetString, title?: string, kind?: DocumentPasteEditKind); + * - Update provide to return multiple edits + */ + export interface DocumentDropEdit { /** * Human readable label that describes the edit. */ - label?: string; + title?: string; + + /** + * {@link DocumentPasteEditKind Kind} of the edit. + * + * Used to identify specific types of edits. + * + * TODO: use own type? + */ + kind: DocumentPasteEditKind; /** * The mime type from the {@link DataTransfer} that this edit applies. @@ -23,22 +39,11 @@ declare module 'vscode' { /** * Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list. */ - yieldTo?: ReadonlyArray< - // TODO: what about built-in providers? - | { readonly extensionId: string; readonly providerId: string } - | { readonly mimeType: string } - >; + yieldTo?: ReadonlyArray<{ readonly kind: DocumentPasteEditKind } | { readonly mimeType: string }>; } export interface DocumentDropEditProviderMetadata { - /** - * Identifies the provider. - * - * This id is used when users configure the default provider for drop. - * - * This id should be unique within the extension but does not need to be unique across extensions. - */ - readonly id: string; + readonly providedDropEditKinds?: readonly DocumentPasteEditKind[]; /** * List of {@link DataTransfer} mime types that the provider can handle. From dcee2edef1e64642004fc3825f4aeea60b9aa17c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 7 Mar 2024 19:08:21 -0800 Subject: [PATCH 062/141] testing: additional context keys and state around test results (#207119) For explorations with copilot later --- src/vs/workbench/api/common/extHostTesting.ts | 6 ++- .../testing/browser/testingOutputPeek.ts | 40 ++++++++++--------- .../contrib/testing/common/testTypes.ts | 12 +++++- .../testing/common/testingContextKeys.ts | 4 ++ .../actions/common/menusExtensionPoint.ts | 5 +++ 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 597865e61c0..5d0ca1cb8b9 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -74,9 +74,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return controller?.collection.tree.get(targetTest)?.actual ?? toItemFromContext(arg); } case MarshalledId.TestMessageMenuArgs: { - const { extId, message } = arg as ITestMessageMenuArgs; + const { test, message } = arg as ITestMessageMenuArgs; + const extId = test.item.extId; return { - test: this.controllers.get(TestId.root(extId))?.collection.tree.get(extId)?.actual, + test: this.controllers.get(TestId.root(extId))?.collection.tree.get(extId)?.actual + ?? toItemFromContext({ $mid: MarshalledId.TestItemContext, tests: [test] }), message: Convert.TestMessage.to(message as ITestErrorMessage.Serialized), }; } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 901d0a5d5da..1d38f8d8389 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -98,13 +98,19 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId } from 'vs/workbench/contrib/testing/common/testTypes'; +import { IRichLocation, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { ParsedTestUri, TestUriType, buildTestUri, parseTestUri } from 'vs/workbench/contrib/testing/common/testingUri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +const getMessageArgs = (test: TestResultItem, message: ITestMessage): ITestMessageMenuArgs => ({ + $mid: MarshalledId.TestMessageMenuArgs, + test: InternalTestItem.serialize(test), + message: ITestMessage.serialize(message), +}); + class MessageSubject { public readonly test: ITestItem; public readonly message: ITestMessage; @@ -112,6 +118,7 @@ class MessageSubject { public readonly actualUri: URI; public readonly messageUri: URI; public readonly revealLocation: IRichLocation | undefined; + public readonly context: ITestMessageMenuArgs | undefined; public get isDiffable() { return this.message.type === TestMessageType.Error && isDiffable(this.message); @@ -121,14 +128,6 @@ class MessageSubject { return this.message.type === TestMessageType.Error ? this.message.contextValue : undefined; } - public get context(): ITestMessageMenuArgs { - return { - $mid: MarshalledId.TestMessageMenuArgs, - extId: this.test.extId, - message: ITestMessage.serialize(this.message), - }; - } - constructor(public readonly result: ITestResult, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) { this.test = test.item; const messages = test.tasks[taskIndex].messages; @@ -140,6 +139,7 @@ class MessageSubject { this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage }); const message = this.message = messages[this.messageIndex]; + this.context = getMessageArgs(test, message); this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined); } } @@ -1745,7 +1745,10 @@ class CoverageElement implements ITreeElement { class TestCaseElement implements ITreeElement { public readonly type = 'test'; - public readonly context = this.test.item.extId; + public readonly context: ITestItemContext = { + $mid: MarshalledId.TestItemContext, + tests: [InternalTestItem.serialize(this.test)], + }; public readonly id = `${this.results.id}/${this.test.item.extId}`; public readonly description?: string; @@ -1826,13 +1829,12 @@ class TestMessageElement implements ITreeElement { } public get context(): ITestMessageMenuArgs { - return { - $mid: MarshalledId.TestMessageMenuArgs, - extId: this.test.item.extId, - message: ITestMessage.serialize(this.message), - }; + return getMessageArgs(this.test, this.message); } + public get outputSubject() { + return new TestOutputSubject(this.result, this.taskIndex, this.test); + } constructor( public readonly result: ITestResult, @@ -2357,11 +2359,10 @@ class TreeActionsProvider { if (element instanceof TestCaseElement || element instanceof TestMessageElement) { contextKeys.push( [TestingContextKeys.testResultOutdated.key, element.test.retired], + [TestingContextKeys.testResultState.key, testResultStateToContextValues[element.test.ownComputedState]], ...getTestItemContextOverlay(element.test, capabilities), ); - } - if (element instanceof TestCaseElement) { const extId = element.test.item.extId; if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { primary.push(new Action( @@ -2401,12 +2402,15 @@ class TreeActionsProvider { )); } + } + + if (element instanceof TestMessageElement) { primary.push(new Action( 'testing.outputPeek.goToFile', localize('testing.goToFile', "Go to Source"), ThemeIcon.asClassName(Codicon.goToFile), undefined, - () => this.commandService.executeCommand('vscode.revealTest', extId), + () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), )); } diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 7737ef78020..c760e14bef2 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -21,6 +21,16 @@ export const enum TestResultState { Errored = 6 } +export const testResultStateToContextValues: { [K in TestResultState]: string } = { + [TestResultState.Unset]: 'unset', + [TestResultState.Queued]: 'queued', + [TestResultState.Running]: 'running', + [TestResultState.Passed]: 'passed', + [TestResultState.Failed]: 'failed', + [TestResultState.Skipped]: 'skipped', + [TestResultState.Errored]: 'errored', +}; + /** note: keep in sync with TestRunProfileKind in vscode.d.ts */ export const enum ExtTestRunProfileKind { Run = 1, @@ -755,7 +765,7 @@ export interface ITestMessageMenuArgs { /** Marshalling marker */ $mid: MarshalledId.TestMessageMenuArgs; /** Tests ext ID */ - extId: string; + test: InternalTestItem.Serialized; /** Serialized test message */ message: ITestMessage.Serialized; } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index cc7821a4e27..ddef4fcdc15 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -67,4 +67,8 @@ export namespace TestingContextKeys { type: 'boolean', description: localize('testing.testResultOutdated', 'Value available in editor/content and testing/message/context when the result is outdated') }); + export const testResultState = new RawContextKey('testResultState', undefined, { + type: 'string', + description: localize('testing.testResultState', 'Value available testing/item/result indicating the state of the item.') + }); } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index c43a6eb3bed..0f0346f9fb3 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -343,6 +343,11 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.TestItemGutter, description: localize('testing.item.gutter.title', "The menu for a gutter decoration for a test item"), }, + { + key: 'testing/item/result', + id: MenuId.TestPeekElement, + description: localize('testing.item.result.title', "The menu for an item in the Test Results view or peek."), + }, { key: 'testing/message/context', id: MenuId.TestMessageContext, From 1c2151afda3afebd46d4e1d86c5d2f5d79363acc Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 8 Mar 2024 00:27:09 -0300 Subject: [PATCH 063/141] Fix expected errors in telemetry (#207128) * Fix #206108 * Fix #206109 --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index ac232bc4dba..ca303ca277d 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; @@ -366,7 +367,7 @@ export class ChatService extends Disposable implements IChatService { const provider = this._providers.get(model.providerId); if (!provider) { - throw new Error(`Unknown provider: ${model.providerId}`); + throw new ErrorNoTelemetry(`Unknown provider: ${model.providerId}`); } let session: IChat | undefined; @@ -384,7 +385,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (!defaultAgent) { - throw new Error('No default agent'); + throw new ErrorNoTelemetry('No default agent'); } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; From 6c19c003574b02afc604c0c1c471b1441b33a3d3 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 7 Mar 2024 20:28:31 -0800 Subject: [PATCH 064/141] Revert "Only one subscriber for kernels for onDidChangeSelectedNotebooks (#204417)" (#207132) This reverts commit 59ec734843767b100e4bd476903e0c363772a1f9. --- .../api/browser/mainThreadNotebookKernels.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index c7d0f3a6611..678825ac6a5 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -7,7 +7,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -151,16 +151,6 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape this._proxy.$cellExecutionChanged(e.notebook, e.cellHandle, e.changed?.state); } })); - - this._disposables.add(this._notebookKernelService.onDidChangeSelectedNotebooks(e => { - for (const [handle, [kernel,]] of this._kernels) { - if (e.oldKernel === kernel.id) { - this._proxy.$acceptNotebookAssociation(handle, e.notebook, false); - } else if (e.newKernel === kernel.id) { - this._proxy.$acceptNotebookAssociation(handle, e.notebook, true); - } - } - })); } dispose(): void { @@ -272,8 +262,16 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape } }(data, this._languageService); + const listener = this._notebookKernelService.onDidChangeSelectedNotebooks(e => { + if (e.oldKernel === kernel.id) { + this._proxy.$acceptNotebookAssociation(handle, e.notebook, false); + } else if (e.newKernel === kernel.id) { + this._proxy.$acceptNotebookAssociation(handle, e.notebook, true); + } + }); + const registration = this._notebookKernelService.registerKernel(kernel); - this._kernels.set(handle, [kernel, registration]); + this._kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); } $updateKernel(handle: number, data: Partial): void { From 59857a62e6e1705a3a3d493e7557cb032a63a1c2 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 8 Mar 2024 12:39:44 +0100 Subject: [PATCH 065/141] Add reply to the comments view context menu (#207156) Fixes microsoft/vscode-pull-request-github#5698 --- .../api/browser/mainThreadComments.ts | 3 +- .../workbench/api/common/extHostComments.ts | 10 +-- src/vs/workbench/common/comments.ts | 17 +++++ .../contrib/comments/browser/commentNode.ts | 3 +- .../comments/browser/commentThreadHeader.ts | 3 +- .../comments/browser/commentThreadWidget.ts | 2 +- .../browser/commentThreadZoneWidget.ts | 16 ++++- .../comments/browser/comments.contribution.ts | 28 +++++++- .../comments/browser/commentsController.ts | 67 +++++++++++++++++-- .../comments/browser/commentsTreeViewer.ts | 11 +-- .../contrib/comments/browser/commentsView.ts | 63 ++--------------- .../contrib/comments/common/commentModel.ts | 26 ++++--- 12 files changed, 161 insertions(+), 88 deletions(-) create mode 100644 src/vs/workbench/common/comments.ts diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 8896e0b6e1c..a5bfd4f0d49 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -26,6 +26,7 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Schemas } from 'vs/base/common/network'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; @@ -197,7 +198,7 @@ export class MainThreadCommentThread implements languages.CommentThread { this._onDidChangeState.dispose(); } - toJSON(): any { + toJSON(): MarshalledCommentThread { return { $mid: MarshalledId.CommentThread, commentControlHandle: this.controllerHandle, diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 3e20208c247..eb85e826f5f 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -20,6 +20,7 @@ import type * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, CommentThreadChanges, CommentChanges } from './extHost.protocol'; import { ExtHostCommands } from './extHostCommands'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; type ProviderHandle = number; @@ -53,16 +54,17 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentController.value; } else if (arg && arg.$mid === MarshalledId.CommentThread) { - const commentController = this._commentControllers.get(arg.commentControlHandle); + const marshalledCommentThread: MarshalledCommentThread = arg; + const commentController = this._commentControllers.get(marshalledCommentThread.commentControlHandle); if (!commentController) { - return arg; + return marshalledCommentThread; } - const commentThread = commentController.getCommentThread(arg.commentThreadHandle); + const commentThread = commentController.getCommentThread(marshalledCommentThread.commentThreadHandle); if (!commentThread) { - return arg; + return marshalledCommentThread; } return commentThread.value; diff --git a/src/vs/workbench/common/comments.ts b/src/vs/workbench/common/comments.ts new file mode 100644 index 00000000000..038819d8f99 --- /dev/null +++ b/src/vs/workbench/common/comments.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { CommentThread } from 'vs/editor/common/languages'; + +export interface MarshalledCommentThread { + $mid: MarshalledId.CommentThread; + commentControlHandle: number; + commentThreadHandle: number; +} + +export interface MarshalledCommentThreadInternal extends MarshalledCommentThread { + thread: CommentThread; +} diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 95ef19e971e..df2afb078e5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -50,6 +50,7 @@ import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/c import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; class CommentsActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: any[]): Promise { @@ -293,7 +294,7 @@ export class CommentNode extends Disposable { return result; } - private get commentNodeContext() { + private get commentNodeContext(): [any, MarshalledCommentThread] { return [{ thread: this.commentThread, commentUniqueId: this.comment.uniqueIdInThread, diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index b206d26b011..9784625cd2f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -22,6 +22,7 @@ import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; const collapseIcon = registerIcon('review-comment-collapse', Codicon.chevronUp, nls.localize('collapseIcon', 'Icon to collapse a review comment.')); const COLLAPSE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(collapseIcon); @@ -122,7 +123,7 @@ export class CommentThreadHeader extends Disposable { getAnchor: () => event, getActions: () => actions, actionRunner: new ActionRunner(), - getActionsContext: () => { + getActionsContext: (): MarshalledCommentThread => { return { commentControlHandle: this._commentThread.controllerHandle, commentThreadHandle: this._commentThread.commentThreadHandle, diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index bcd9366e524..e09cd4f5167 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -351,7 +351,7 @@ export class CommentThreadWidget extends } focusCommentEditor() { - this._commentReply?.focusCommentEditor(); + this._commentReply?.expandReplyAreaAndFocusCommentEditor(); } focus() { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index baa3438345d..a3c6f70f09a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -31,6 +31,12 @@ function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | return getCommentThreadStateBorderColor(thread, theme) ?? theme.getColor(peekViewBorder); } +export enum CommentWidgetFocus { + None = 0, + Widget = 1, + Editor = 2 +} + export function parseMouseDownInfoFromEvent(e: IEditorMouseEvent) { const range = e.target.range; @@ -181,7 +187,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget // we don't do anything here as we always do the reveal ourselves. } - public reveal(commentUniqueId?: number, focus: boolean = false) { + public reveal(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { if (!this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); } @@ -197,16 +203,20 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; } this.editor.setScrollTop(scrollTop); - if (focus) { + if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); + } else if (focus === CommentWidgetFocus.Editor) { + this._commentThreadWidget.focusCommentEditor(); } return; } } this.editor.revealRangeInCenter(this._commentThread.range ?? new Range(1, 1, 1, 1)); - if (focus) { + if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); + } else if (focus === CommentWidgetFocus.Editor) { + this._commentThreadWidget.focusCommentEditor(); } } diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index b47cdb2b883..e57e7c315e2 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -17,10 +17,14 @@ import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/co import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { CommentThreadState } from 'vs/editor/common/languages'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CONTEXT_KEY_HAS_COMMENTS, CONTEXT_KEY_SOME_COMMENTS_EXPANDED, CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { Codicon } from 'vs/base/common/codicons'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; registerAction2(class Collapse extends ViewAction { constructor() { @@ -64,6 +68,28 @@ registerAction2(class Expand extends ViewAction { } }); +registerAction2(class Reply extends Action2 { + constructor() { + super({ + id: 'comments.reply', + title: nls.localize('reply', "Reply"), + icon: Codicon.reply, + menu: { + id: MenuId.CommentsViewThreadActions, + order: 100, + when: ContextKeyExpr.equals('canReply', true) + }, + }); + } + + override run(accessor: ServicesAccessor, marshalledCommentThread: MarshalledCommentThreadInternal): void { + const commentService = accessor.get(ICommentService); + const editorService = accessor.get(IEditorService); + const uriIdentityService = accessor.get(IUriIdentityService); + revealCommentThread(commentService, editorService, uriIdentityService, marshalledCommentThread.thread, marshalledCommentThread.thread.comments![marshalledCommentThread.thread.comments!.length - 1], true); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'comments', order: 20, diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index e83cb560c6c..7abfde48305 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -10,12 +10,12 @@ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/com import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/review'; -import { ICodeEditor, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { EditorType, IDiffEditor, IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon'; +import { EditorType, IDiffEditor, IEditor, IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import * as languages from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -23,8 +23,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CommentWidgetFocus, isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -45,6 +45,8 @@ import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/commo import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { URI } from 'vs/base/common/uri'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; export const ID = 'editor.contrib.review'; @@ -366,6 +368,57 @@ class CommentingRangeDecorator { } } +export function revealCommentThread(commentService: ICommentService, editorService: IEditorService, uriIdentityService: IUriIdentityService, + commentThread: languages.CommentThread, comment: languages.Comment, focusReply?: boolean, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { + if (!commentThread.resource) { + return; + } + if (!commentService.isCommentingEnabled) { + commentService.enableCommenting(true); + } + + const range = commentThread.range; + const focus = focusReply ? CommentWidgetFocus.Editor : (preserveFocus ? CommentWidgetFocus.None : CommentWidgetFocus.Widget); + + const activeEditor = editorService.activeTextEditorControl; + // If the active editor is a diff editor where one of the sides has the comment, + // then we try to reveal the comment in the diff editor. + const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] + : (activeEditor ? [activeEditor] : []); + const threadToReveal = commentThread.threadId; + const commentToReveal = comment.uniqueIdInThread; + const resource = URI.parse(commentThread.resource); + + for (const editor of currentActiveResources) { + const model = editor.getModel(); + if ((model instanceof TextModel) && uriIdentityService.extUri.isEqual(resource, model.uri)) { + + if (threadToReveal && isCodeEditor(editor)) { + const controller = CommentController.get(editor); + controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus); + } + return; + } + } + + editorService.openEditor({ + resource, + options: { + pinned: pinned, + preserveFocus: preserveFocus, + selection: range ?? new Range(1, 1, 1, 1) + } + } as ITextResourceEditorInput, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + if (editor) { + const control = editor.getControl(); + if (threadToReveal && isCodeEditor(control)) { + const controller = CommentController.get(control); + controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus); + } + } + }); +} + export class CommentController implements IEditorContribution { private readonly globalToDispose = new DisposableStore(); private readonly localToDispose = new DisposableStore(); @@ -630,7 +683,7 @@ export class CommentController implements IEditorContribution { return editor.getContribution(ID); } - public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean, focus: boolean): void { + public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean, focus: CommentWidgetFocus): void { const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId); if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentUniqueId, focus); @@ -734,7 +787,7 @@ export class CommentController implements IEditorContribution { nextWidget = sortedWidgets[idx]; } this.editor.setSelection(nextWidget.commentThread.range ?? new Range(1, 1, 1, 1)); - nextWidget.reveal(undefined, true); + nextWidget.reveal(undefined, CommentWidgetFocus.Widget); } public previousCommentThread(): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index df131acad4b..147d7ed6fab 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -42,6 +42,7 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MarshalledCommentThread, MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_STORAGE_ID = 'Comments'; @@ -161,7 +162,8 @@ class CommentsMenus implements IDisposable { const overlay: [string, any][] = [ ['commentController', element.owner], ['resourceScheme', element.resource.scheme], - ['commentThread', element.contextValue] + ['commentThread', element.contextValue], + ['canReply', element.thread.canReply] ]; const contextKeyService = this.contextKeyService.createOverlay(overlay); @@ -304,7 +306,7 @@ export class CommentNodeRenderer implements IListRenderer commentControlHandle: node.element.controllerHandle, commentThreadHandle: node.element.threadHandle, $mid: MarshalledId.CommentThread - }; + } as MarshalledCommentThread; if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; @@ -511,10 +513,11 @@ export class CommentsList extends WorkbenchObjectTree ({ + getActionsContext: (): MarshalledCommentThreadInternal => ({ commentControlHandle: node.controllerHandle, commentThreadHandle: node.threadHandle, - $mid: MarshalledId.CommentThread + $mid: MarshalledId.CommentThread, + thread: node.thread }) }); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 2530cf96e7c..b45f12a0cb7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -7,12 +7,11 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { basename } from 'vs/base/common/resources'; -import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { IViewPaneOptions, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -24,8 +23,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IEditor } from 'vs/editor/common/editorCommon'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contrib/comments/browser/commentsViewActions'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; @@ -34,8 +31,7 @@ import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFil import { CommentThreadState } from 'vs/editor/common/languages'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { Iterable } from 'vs/base/common/iterator'; -import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController'; -import { Range } from 'vs/editor/common/core/range'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; @@ -323,62 +319,17 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { })); } - private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): boolean { + private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { if (!element) { - return false; + return; } if (!(element instanceof ResourceWithCommentThreads || element instanceof CommentNode)) { - return false; + return; } - - if (!this.commentService.isCommentingEnabled) { - this.commentService.enableCommenting(true); - } - - const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range; - - const activeEditor = this.editorService.activeTextEditorControl; - // If the active editor is a diff editor where one of the sides has the comment, - // then we try to reveal the comment in the diff editor. - const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] - : (activeEditor ? [activeEditor] : []); - - for (const editor of currentActiveResources) { - const model = editor.getModel(); - if ((model instanceof TextModel) && this.uriIdentityService.extUri.isEqual(element.resource, model.uri)) { - const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; - const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; - if (threadToReveal && isCodeEditor(editor)) { - const controller = CommentController.get(editor); - controller?.revealCommentThread(threadToReveal, commentToReveal, true, !preserveFocus); - } - - return true; - } - } - - const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; + const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].thread : element.thread; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : element.comment; - - this.editorService.openEditor({ - resource: element.resource, - options: { - pinned: pinned, - preserveFocus: preserveFocus, - selection: range ?? new Range(1, 1, 1, 1) - } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { - if (editor) { - const control = editor.getControl(); - if (threadToReveal && isCodeEditor(control)) { - const controller = CommentController.get(control); - controller?.revealCommentThread(threadToReveal, commentToReveal.uniqueIdInThread, true, !preserveFocus); - } - } - }); - - return true; + return revealCommentThread(this.commentService, this.editorService, this.uriIdentityService, threadToReveal, commentToReveal, false, pinned, preserveFocus, sideBySide); } private async refresh(): Promise { diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index e4418a28dfc..7bf9efe417a 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -16,18 +16,26 @@ export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent new CommentNode(uniqueOwner, threadId, resource, comment, range, commentThread.state, commentThread.contextValue, owner, commentThread.controllerHandle, commentThread.commentThreadHandle)); + const { comments } = commentThread; + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(uniqueOwner, owner, resource, comment, commentThread)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } From c37ca48a96e37e30e13ed4d4679bca1c2e330ad4 Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 8 Mar 2024 21:48:21 +0900 Subject: [PATCH 066/141] chore: update node-pty@1.1.0-beta11 (#207153) --- package.json | 2 +- remote/package.json | 2 +- remote/yarn.lock | 8 ++++---- yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index fae4fcc57a8..caf83733802 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "native-is-elevated": "0.7.0", "native-keymap": "^3.3.4", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta6", + "node-pty": "1.1.0-beta11", "tas-client-umd": "0.1.8", "v8-inspect-profiler": "^0.1.0", "vscode-oniguruma": "1.7.0", diff --git a/remote/package.json b/remote/package.json index 2dcfab99b56..56e7f9e975e 100644 --- a/remote/package.json +++ b/remote/package.json @@ -29,7 +29,7 @@ "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta6", + "node-pty": "1.1.0-beta11", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/remote/yarn.lock b/remote/yarn.lock index b068bf655bb..63417331620 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -431,10 +431,10 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-pty@1.1.0-beta6: - version "1.1.0-beta6" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta6.tgz#8b27ce40268e313868925e1b46f2af98cc677881" - integrity sha512-ZcuPz5wIbfF4rebVv8sl+nf2Cn5dVMqlEl9PtabCt4uIffGDnovOpmwh16Oh/MThrwSmeJL6gBwu6lIbBtW7DQ== +node-pty@1.1.0-beta11: + version "1.1.0-beta11" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta11.tgz#909d5dd8f9aa2a7857e7b632fd4d2d4768bdf69f" + integrity sha512-vTjF+VrvSCfPDILUkIT+YrG1Fdn06/eBRS2fc9a3JzYAvknMB1Ip8aoJhxl8hNpjWAbprmCEiV91mlfNpCD+GQ== dependencies: node-addon-api "^7.1.0" diff --git a/yarn.lock b/yarn.lock index a7e82c60be5..a97562951bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6939,10 +6939,10 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-pty@1.1.0-beta6: - version "1.1.0-beta6" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta6.tgz#8b27ce40268e313868925e1b46f2af98cc677881" - integrity sha512-ZcuPz5wIbfF4rebVv8sl+nf2Cn5dVMqlEl9PtabCt4uIffGDnovOpmwh16Oh/MThrwSmeJL6gBwu6lIbBtW7DQ== +node-pty@1.1.0-beta11: + version "1.1.0-beta11" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta11.tgz#909d5dd8f9aa2a7857e7b632fd4d2d4768bdf69f" + integrity sha512-vTjF+VrvSCfPDILUkIT+YrG1Fdn06/eBRS2fc9a3JzYAvknMB1Ip8aoJhxl8hNpjWAbprmCEiV91mlfNpCD+GQ== dependencies: node-addon-api "^7.1.0" From 27714e45c10df52ac2d9c6bf9b79e10fef00fb51 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:03:06 +0100 Subject: [PATCH 067/141] Include pointer size in hover bounding box computation (#207092) Include pointer size in window/hover bounding box computation --- .../services/hoverService/hoverWidget.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index 4ec59ed09bd..24ae9cf483e 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -469,9 +469,11 @@ export class HoverWidget extends Widget implements IHoverWidget { return; } + const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0); + // When force position is enabled, restrict max width if (this._forcePosition) { - const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth; + const padding = hoverPointerOffset + Constants.HoverBorderWidth; if (this._hoverPosition === HoverPosition.RIGHT) { this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`; } else if (this._hoverPosition === HoverPosition.LEFT) { @@ -484,10 +486,10 @@ export class HoverWidget extends Widget implements IHoverWidget { if (this._hoverPosition === HoverPosition.RIGHT) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // Hover on the right is going beyond window. - if (roomOnRight < this._hover.containerDomNode.clientWidth) { + if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnLeft = target.left; // There's enough room on the left, flip the hover position - if (roomOnLeft >= this._hover.containerDomNode.clientWidth) { + if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = HoverPosition.LEFT; } // Hover on the left would go beyond window too @@ -501,10 +503,10 @@ export class HoverWidget extends Widget implements IHoverWidget { const roomOnLeft = target.left; // Hover on the left is going beyond window. - if (roomOnLeft < this._hover.containerDomNode.clientWidth) { + if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // There's enough room on the right, flip the hover position - if (roomOnRight >= this._hover.containerDomNode.clientWidth) { + if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = HoverPosition.RIGHT; } // Hover on the right would go beyond window too @@ -513,7 +515,7 @@ export class HoverWidget extends Widget implements IHoverWidget { } } // Hover on the left is going beyond window. - if (target.left - this._hover.containerDomNode.clientWidth <= this._targetDocumentElement.clientLeft) { + if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) { this._hoverPosition = HoverPosition.RIGHT; } } @@ -526,10 +528,12 @@ export class HoverWidget extends Widget implements IHoverWidget { return; } + const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0); + // Position hover on top of the target if (this._hoverPosition === HoverPosition.ABOVE) { // Hover on top is going beyond window - if (target.top - this._hover.containerDomNode.clientHeight < 0) { + if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) { this._hoverPosition = HoverPosition.BELOW; } } @@ -537,7 +541,7 @@ export class HoverWidget extends Widget implements IHoverWidget { // Position hover below the target else if (this._hoverPosition === HoverPosition.BELOW) { // Hover on bottom is going beyond window - if (target.bottom + this._hover.containerDomNode.clientHeight > this._targetWindow.innerHeight) { + if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) { this._hoverPosition = HoverPosition.ABOVE; } } From 5c6765c8f2abae365dcf256b46f6b1ae8ea8b9d2 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:09:33 +0100 Subject: [PATCH 068/141] Fix active pane indicator issues (#207163) fix active pane indicator issues --- .../parts/auxiliarybar/media/auxiliaryBarPart.css | 9 ++++++++- .../workbench/browser/parts/media/paneCompositePart.css | 2 +- .../browser/parts/sidebar/media/sidebarpart.css | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index ad3e53ded5a..675a6507271 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -34,21 +34,28 @@ flex: 1; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { - border-top-color: var(--vscode-panelTitle-activeBorder) !important; + border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { color: var(--vscode-sideBarTitle-foreground) !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 66bc8d9067e..3b9e71b9182 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -271,7 +271,7 @@ content: ""; position: absolute; z-index: 1; - bottom: 0; + bottom: 2px; width: 100%; height: 0; border-top-width: 1px; diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 65f962e8241..4721db76de4 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -60,11 +60,15 @@ height: 16px; } +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { color: var(--vscode-activityBarTop-foreground) !important; From 9730949668f18f92ef2681ca87b4c017a95da8bf Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:30:33 +0100 Subject: [PATCH 069/141] Fix badge position (#207165) fix badge position --- src/vs/workbench/browser/parts/media/paneCompositePart.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 3b9e71b9182..e3502c3af86 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -219,7 +219,7 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { position: absolute; - top: 11px; + top: 17px; right: 0px; font-size: 9px; font-weight: 600; From a6e2b25575da90a1bcfabc0693277b0ad3407023 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 6 Mar 2024 17:57:50 +0100 Subject: [PATCH 070/141] Moves Permutation class to arrays.ts --- src/vs/base/common/arrays.ts | 33 +++++++++++++++++++ .../browser/inlineCompletionsModel.ts | 3 +- .../inlineCompletions/browser/utils.ts | 23 +------------ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 9b510f82251..f8804af0fe7 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -859,3 +859,36 @@ export class CallbackIterable { return result; } } + +/** + * Represents a re-arrangement of items in an array. + */ +export class Permutation { + constructor(private readonly _indexMap: readonly number[]) { } + + /** + * Returns a permutation that sorts the given array according to the given compare function. + */ + public static createSortPermutation(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation { + const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2])); + return new Permutation(sortIndices); + } + + /** + * Returns a new array with the elements of the given array re-arranged according to this permutation. + */ + apply(arr: readonly T[]): T[] { + return arr.map((_, index) => arr[this._indexMap[index]]); + } + + /** + * Returns a new permutation that undoes the re-arrangement of this permutation. + */ + inverse(): Permutation { + const inverseIndexMap = this._indexMap.slice(); + for (let i = 0; i < this._indexMap.length; i++) { + inverseIndexMap[this._indexMap[i]] = i; + } + return new Permutation(inverseIndexMap); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 5b26d770005..8bc30856f20 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Permutation } from 'vs/base/common/arrays'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -22,7 +23,7 @@ import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostT import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; -import { Permutation, addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 4a9e78238b6..0dc96bb3945 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy } from 'vs/base/common/arrays'; +import { Permutation, compareBy } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorunOpts } from 'vs/base/common/observable'; @@ -161,24 +161,3 @@ export function inverseEdits(model: TextModel, edits: ISingleEditOperation[]): I } return inverseEdits; } - -export class Permutation { - constructor(private readonly _indexMap: number[]) { } - - public static createSortPermutation(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation { - const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2])); - return new Permutation(sortIndices); - } - - apply(arr: readonly T[]): T[] { - return arr.map((_, index) => arr[this._indexMap[index]]); - } - - inverse(): Permutation { - const inverseIndexMap = this._indexMap.slice(); - for (let i = 0; i < this._indexMap.length; i++) { - inverseIndexMap[this._indexMap[i]] = i; - } - return new Permutation(inverseIndexMap); - } -} From 655a48b67ed279ad765c91381ba109340515f8c3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 Mar 2024 14:56:56 +0100 Subject: [PATCH 071/141] Introduces RangeLength --- .../editor/browser/widget/diffEditor/utils.ts | 10 +-- src/vs/editor/common/core/rangeLength.ts | 76 +++++++++++++++++ .../beforeEditPositionMapper.ts | 11 +-- .../bracketPairsTree/length.ts | 84 ++----------------- .../mergeEditor/browser/model/rangeUtils.ts | 16 ++-- .../mergeEditor/browser/view/lineAlignment.ts | 6 +- .../mergeEditor/test/browser/mapping.test.ts | 12 +-- 7 files changed, 112 insertions(+), 103 deletions(-) create mode 100644 src/vs/editor/common/core/rangeLength.ts diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index b71da8bb55d..547392b4ba4 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -425,15 +425,15 @@ export function translatePosition(posInOriginal: Position, mappings: DetailedLin } } -function lengthBetweenPositions(position1: Position, position2: Position): LengthObj { +function lengthBetweenPositions(position1: Position, position2: Position): RangeLength { if (position1.lineNumber === position2.lineNumber) { - return new LengthObj(0, position2.column - position1.column); + return new RangeLength(0, position2.column - position1.column); } else { - return new LengthObj(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } -function addLength(position: Position, length: LengthObj): Position { +function addLength(position: Position, length: RangeLength): Position { if (length.lineCount === 0) { return new Position(position.lineNumber, position.column + length.columnCount); } else { diff --git a/src/vs/editor/common/core/rangeLength.ts b/src/vs/editor/common/core/rangeLength.ts new file mode 100644 index 00000000000..0e505318b05 --- /dev/null +++ b/src/vs/editor/common/core/rangeLength.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a non-negative length of text in terms of line and column count. +*/ +export class RangeLength { + public static zero = new RangeLength(0, 0); + + public static lengthDiffNonNegative(start: RangeLength, end: RangeLength): RangeLength { + if (end.isLessThan(start)) { + return RangeLength.zero; + } + if (start.lineCount === end.lineCount) { + return new RangeLength(0, end.columnCount - start.columnCount); + } else { + return new RangeLength(end.lineCount - start.lineCount, end.columnCount); + } + } + + constructor( + public readonly lineCount: number, + public readonly columnCount: number + ) { } + + public isZero() { + return this.lineCount === 0 && this.columnCount === 0; + } + + public isLessThan(other: RangeLength): boolean { + if (this.lineCount !== other.lineCount) { + return this.lineCount < other.lineCount; + } + return this.columnCount < other.columnCount; + } + + public isGreaterThan(other: RangeLength): boolean { + if (this.lineCount !== other.lineCount) { + return this.lineCount > other.lineCount; + } + return this.columnCount > other.columnCount; + } + + public equals(other: RangeLength): boolean { + return this.lineCount === other.lineCount && this.columnCount === other.columnCount; + } + + public compare(other: RangeLength): number { + if (this.lineCount !== other.lineCount) { + return this.lineCount - other.lineCount; + } + return this.columnCount - other.columnCount; + } + + public add(other: RangeLength): RangeLength { + if (other.lineCount === 0) { + return new RangeLength(this.lineCount, this.columnCount + other.columnCount); + } else { + return new RangeLength(this.lineCount + other.lineCount, other.columnCount); + } + } + + public createRange(startPosition: Position): Range { + if (this.lineCount === 0) { + return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column + this.columnCount); + } else { + return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber + this.lineCount, this.columnCount + 1); + } + } + + toString() { + return `${this.lineCount},${this.columnCount}`; + } +} diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index 501aa07c39b..e596c00e983 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Range } from 'vs/editor/common/core/range'; -import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthOfString, lengthToObj, positionToLength, toLength } from './length'; +import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, lengthOfString, lengthToObj, positionToLength, toLength } from './length'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; import { IModelContentChange } from 'vs/editor/common/textModelEvents'; export class TextEditInfo { @@ -73,7 +74,7 @@ export class BeforeEditPositionMapper { return lengthDiffNonNegative(offset, nextChangeOffset); } - private translateOldToCur(oldOffsetObj: LengthObj): Length { + private translateOldToCur(oldOffsetObj: RangeLength): Length { if (oldOffsetObj.lineCount === this.deltaLineIdxInOld) { return toLength(oldOffsetObj.lineCount + this.deltaOldToNewLineCount, oldOffsetObj.columnCount + this.deltaOldToNewColumnCount); } else { @@ -126,9 +127,9 @@ class TextEditInfoCache { return new TextEditInfoCache(edit.startOffset, edit.endOffset, edit.newLength); } - public readonly endOffsetBeforeObj: LengthObj; - public readonly endOffsetAfterObj: LengthObj; - public readonly offsetObj: LengthObj; + public readonly endOffsetBeforeObj: RangeLength; + public readonly endOffsetAfterObj: RangeLength; + public readonly offsetObj: RangeLength; constructor( startOffset: Length, diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index 40cb0255688..0ff10813857 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts @@ -6,75 +6,7 @@ import { splitLines } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; - -/** - * Represents a non-negative length in terms of line and column count. - * Prefer using {@link Length} for performance reasons. -*/ -export class LengthObj { - public static zero = new LengthObj(0, 0); - - public static lengthDiffNonNegative(start: LengthObj, end: LengthObj): LengthObj { - if (end.isLessThan(start)) { - return LengthObj.zero; - } - if (start.lineCount === end.lineCount) { - return new LengthObj(0, end.columnCount - start.columnCount); - } else { - return new LengthObj(end.lineCount - start.lineCount, end.columnCount); - } - } - - constructor( - public readonly lineCount: number, - public readonly columnCount: number - ) { } - - public isZero() { - return this.lineCount === 0 && this.columnCount === 0; - } - - public toLength(): Length { - return toLength(this.lineCount, this.columnCount); - } - - public isLessThan(other: LengthObj): boolean { - if (this.lineCount !== other.lineCount) { - return this.lineCount < other.lineCount; - } - return this.columnCount < other.columnCount; - } - - public isGreaterThan(other: LengthObj): boolean { - if (this.lineCount !== other.lineCount) { - return this.lineCount > other.lineCount; - } - return this.columnCount > other.columnCount; - } - - public equals(other: LengthObj): boolean { - return this.lineCount === other.lineCount && this.columnCount === other.columnCount; - } - - public compare(other: LengthObj): number { - if (this.lineCount !== other.lineCount) { - return this.lineCount - other.lineCount; - } - return this.columnCount - other.columnCount; - } - - public add(other: LengthObj): LengthObj { - if (other.lineCount === 0) { - return new LengthObj(this.lineCount, this.columnCount + other.columnCount); - } else { - return new LengthObj(this.lineCount + other.lineCount, other.columnCount); - } - } - - toString() { - return `${this.lineCount},${this.columnCount}`; - } -} +import { RangeLength } from 'vs/editor/common/core/rangeLength'; /** * The end must be greater than or equal to the start. @@ -117,11 +49,11 @@ export function toLength(lineCount: number, columnCount: number): Length { return (lineCount * factor + columnCount) as any as Length; } -export function lengthToObj(length: Length): LengthObj { +export function lengthToObj(length: Length): RangeLength { const l = length as any as number; const lineCount = Math.floor(l / factor); const columnCount = l - lineCount * factor; - return new LengthObj(lineCount, columnCount); + return new RangeLength(lineCount, columnCount); } export function lengthGetLineCount(length: Length): number { @@ -216,11 +148,11 @@ export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range { return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1); } -export function lengthOfRange(range: Range): LengthObj { +export function lengthOfRange(range: Range): RangeLength { if (range.startLineNumber === range.endLineNumber) { - return new LengthObj(0, range.endColumn - range.startColumn); + return new RangeLength(0, range.endColumn - range.startColumn); } else { - return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new RangeLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } @@ -235,9 +167,9 @@ export function lengthOfString(str: string): Length { return toLength(lines.length - 1, lines[lines.length - 1].length); } -export function lengthOfStringObj(str: string): LengthObj { +export function lengthOfStringObj(str: string): RangeLength { const lines = splitLines(str); - return new LengthObj(lines.length - 1, lines[lines.length - 1].length); + return new RangeLength(lines.length - 1, lines[lines.length - 1].length); } /** diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts index 92f242a2597..fef5aaad5d7 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts @@ -5,7 +5,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; export function rangeContainsPosition(range: Range, position: Position): boolean { if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { @@ -20,23 +20,23 @@ export function rangeContainsPosition(range: Range, position: Position): boolean return true; } -export function lengthOfRange(range: Range): LengthObj { +export function lengthOfRange(range: Range): RangeLength { if (range.startLineNumber === range.endLineNumber) { - return new LengthObj(0, range.endColumn - range.startColumn); + return new RangeLength(0, range.endColumn - range.startColumn); } else { - return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new RangeLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } -export function lengthBetweenPositions(position1: Position, position2: Position): LengthObj { +export function lengthBetweenPositions(position1: Position, position2: Position): RangeLength { if (position1.lineNumber === position2.lineNumber) { - return new LengthObj(0, position2.column - position1.column); + return new RangeLength(0, position2.column - position1.column); } else { - return new LengthObj(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } -export function addLength(position: Position, length: LengthObj): Position { +export function addLength(position: Position, length: RangeLength): Position { if (length.lineCount === 0) { return new Position(position.lineNumber, position.column + length.columnCount); } else { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts index f6ccc0cf7a3..6d4f28beb1e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts @@ -8,7 +8,7 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { isDefined } from 'vs/base/common/types'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; import { RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { addLength, lengthBetweenPositions, lengthOfRange } from 'vs/workbench/contrib/mergeEditor/browser/model/rangeUtils'; @@ -49,7 +49,7 @@ export function getAlignments(m: ModifiedBaseRange): LineAlignment[] { if (shouldAdd) { result.push(lineAlignment); } else { - if (m.length.isGreaterThan(new LengthObj(1, 0))) { + if (m.length.isGreaterThan(new RangeLength(1, 0))) { result.push([ m.output1Pos ? m.output1Pos.lineNumber + 1 : undefined, m.inputPos.lineNumber + 1, @@ -75,7 +75,7 @@ interface CommonRangeMapping { output1Pos: Position | undefined; output2Pos: Position | undefined; inputPos: Position; - length: LengthObj; + length: RangeLength; } function toEqualRangeMappings(diffs: RangeMapping[], inputRange: Range, outputRange: Range): RangeMapping[] { diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index ae714c10766..9d824d4c7fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; import { DocumentRangeMap, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; suite('merge editor mapping', () => { @@ -53,19 +53,19 @@ function parsePos(str: string): Position { return new Position(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function parseLengthObj(str: string): LengthObj { +function parseLengthObj(str: string): RangeLength { const [lineCount, columnCount] = str.split(':'); - return new LengthObj(parseInt(lineCount, 10), parseInt(columnCount, 10)); + return new RangeLength(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function toPosition(length: LengthObj): Position { +function toPosition(length: RangeLength): Position { return new Position(length.lineCount + 1, length.columnCount + 1); } function createDocumentRangeMap(items: ([string, string] | string)[]) { const mappings: RangeMapping[] = []; - let lastLen1 = new LengthObj(0, 0); - let lastLen2 = new LengthObj(0, 0); + let lastLen1 = new RangeLength(0, 0); + let lastLen2 = new RangeLength(0, 0); for (const item of items) { if (typeof item === 'string') { const len = parseLengthObj(item); From a278ef641dae4291f6d403eaf55acdc973fcf168 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 Mar 2024 16:44:35 +0100 Subject: [PATCH 072/141] Moves utilities into RangeLength --- .../editor/browser/widget/diffEditor/utils.ts | 10 +----- src/vs/editor/common/core/rangeLength.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 547392b4ba4..ebe47afd423 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -421,7 +421,7 @@ export function translatePosition(posInOriginal: Position, mappings: DetailedLin return innerMapping.modifiedRange; } else { const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal); - return Range.fromPositions(addLength(innerMapping.modifiedRange.getEndPosition(), l)); + return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition())); } } @@ -433,14 +433,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): Range } } -function addLength(position: Position, length: RangeLength): Position { - if (length.lineCount === 0) { - return new Position(position.lineNumber, position.column + length.columnCount); - } else { - return new Position(position.lineNumber + length.lineCount, length.columnCount + 1); - } -} - export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { const boundKey = key.bindTo(service); return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { diff --git a/src/vs/editor/common/core/rangeLength.ts b/src/vs/editor/common/core/rangeLength.ts index 0e505318b05..90c47e092c9 100644 --- a/src/vs/editor/common/core/rangeLength.ts +++ b/src/vs/editor/common/core/rangeLength.ts @@ -2,6 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; /** * Represents a non-negative length of text in terms of line and column count. @@ -20,6 +22,32 @@ export class RangeLength { } } + public static betweenPositions(position1: Position, position2: Position): RangeLength { + if (position1.lineNumber === position2.lineNumber) { + return new RangeLength(0, position2.column - position1.column); + } else { + return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); + } + } + + public static ofRange(range: Range) { + return RangeLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); + } + + public static ofText(text: string): RangeLength { + let line = 0; + let column = 0; + for (const c of text) { + if (c === '\n') { + line++; + column = 0; + } else { + column++; + } + } + return new RangeLength(line, column); + } + constructor( public readonly lineCount: number, public readonly columnCount: number @@ -70,6 +98,14 @@ export class RangeLength { } } + public addToPosition(position: Position): Position { + if (this.lineCount === 0) { + return new Position(position.lineNumber, position.column + this.columnCount); + } else { + return new Position(position.lineNumber + this.lineCount, this.columnCount + 1); + } + } + toString() { return `${this.lineCount},${this.columnCount}`; } From a9cd99e83e039da90e915e9e27485a089a9b4449 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 Mar 2024 16:50:02 +0100 Subject: [PATCH 073/141] Introduces core TextEdit --- src/vs/editor/common/core/positionToOffset.ts | 48 ++++ src/vs/editor/common/core/rangeMapping.ts | 73 ++++++ src/vs/editor/common/core/textEdit.ts | 194 +++++++++++++++ src/vs/editor/common/model/textModel.ts | 10 +- src/vs/editor/common/model/textModelText.ts | 23 ++ .../inlineCompletions/browser/ghostText.ts | 43 ++-- .../browser/inlineCompletionsModel.ts | 23 +- .../browser/inlineCompletionsSource.ts | 5 +- .../browser/provideInlineCompletions.ts | 2 +- .../browser/singleTextEdit.ts | 223 +++++++++--------- .../suggestWidgetInlineCompletionProvider.ts | 10 +- .../inlineCompletions/browser/utils.ts | 105 +-------- .../browser/inlineCompletionsModel.test.ts | 2 +- .../browser/inlineCompletionsProvider.test.ts | 5 +- .../test/browser/utils.test.ts | 33 --- .../inlineCompletions/test/browser/utils.ts | 20 -- .../core/positionOffsetTransformer.test.ts | 59 +++++ .../editor/test/common/core/textEdit.test.ts | 62 +++++ .../combineTextEditInfos.test.ts | 5 +- 19 files changed, 622 insertions(+), 323 deletions(-) create mode 100644 src/vs/editor/common/core/positionToOffset.ts create mode 100644 src/vs/editor/common/core/rangeMapping.ts create mode 100644 src/vs/editor/common/core/textEdit.ts create mode 100644 src/vs/editor/common/model/textModelText.ts delete mode 100644 src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts create mode 100644 src/vs/editor/test/common/core/positionOffsetTransformer.test.ts create mode 100644 src/vs/editor/test/common/core/textEdit.test.ts diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts new file mode 100644 index 00000000000..c4abee8a332 --- /dev/null +++ b/src/vs/editor/common/core/positionToOffset.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findLastIdxMonotonous } from 'vs/base/common/arraysFind'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; + +export class PositionOffsetTransformer { + private readonly lineStartOffsetByLineIdx: number[]; + + constructor(text: string) { + this.lineStartOffsetByLineIdx = []; + this.lineStartOffsetByLineIdx.push(0); + for (let i = 0; i < text.length; i++) { + if (text.charAt(i) === '\n') { + this.lineStartOffsetByLineIdx.push(i + 1); + } + } + } + + getOffset(position: Position): number { + return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; + } + + getOffsetRange(range: Range): OffsetRange { + return new OffsetRange( + this.getOffset(range.getStartPosition()), + this.getOffset(range.getEndPosition()) + ); + } + + getPosition(offset: number): Position { + const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset); + const lineNumber = idx + 1; + const column = offset - this.lineStartOffsetByLineIdx[idx] + 1; + return new Position(lineNumber, column); + } + + getRange(offsetRange: OffsetRange): Range { + return Range.fromPositions( + this.getPosition(offsetRange.start), + this.getPosition(offsetRange.endExclusive) + ); + } +} diff --git a/src/vs/editor/common/core/rangeMapping.ts b/src/vs/editor/common/core/rangeMapping.ts new file mode 100644 index 00000000000..6e87efee7f7 --- /dev/null +++ b/src/vs/editor/common/core/rangeMapping.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findLastMonotonous } from 'vs/base/common/arraysFind'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; + +/** + * Represents a list of mappings of ranges from one document to another. + */ +export class RangeMapping { + constructor(public readonly mappings: readonly SingleRangeMapping[]) { + } + + mapPosition(position: Position): PositionOrRange { + const mapping = findLastMonotonous(this.mappings, m => m.original.getStartPosition().isBeforeOrEqual(position)); + if (!mapping) { + return PositionOrRange.position(position); + } + if (mapping.original.containsPosition(position)) { + return PositionOrRange.range(mapping.modified); + } + const l = RangeLength.betweenPositions(mapping.original.getEndPosition(), position); + return PositionOrRange.position(l.addToPosition(mapping.modified.getEndPosition())); + } + + mapRange(range: Range): Range { + const start = this.mapPosition(range.getStartPosition()); + const end = this.mapPosition(range.getEndPosition()); + return Range.fromPositions( + start.range?.getStartPosition() ?? start.position!, + end.range?.getEndPosition() ?? end.position!, + ); + } + + reverse(): RangeMapping { + return new RangeMapping(this.mappings.map(mapping => mapping.reverse())); + } +} + +export class SingleRangeMapping { + constructor( + public readonly original: Range, + public readonly modified: Range, + ) { + } + + reverse(): SingleRangeMapping { + return new SingleRangeMapping(this.modified, this.original); + } + + toString() { + return `${this.original.toString()} -> ${this.modified.toString()}`; + } +} + +export class PositionOrRange { + public static position(position: Position): PositionOrRange { + return new PositionOrRange(position, undefined); + } + + public static range(range: Range): PositionOrRange { + return new PositionOrRange(undefined, range); + } + + private constructor( + public readonly position: Position | undefined, + public readonly range: Range | undefined, + ) { } +} diff --git a/src/vs/editor/common/core/textEdit.ts b/src/vs/editor/common/core/textEdit.ts new file mode 100644 index 00000000000..42e94c281d7 --- /dev/null +++ b/src/vs/editor/common/core/textEdit.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Position } from 'vs/editor/common/core/position'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; +import { Range } from 'vs/editor/common/core/range'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; + +export class TextEdit { + constructor(public readonly edits: readonly SingleTextEdit[]) { + assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); + } + + mapPosition(position: Position): Position | Range { + let lineDelta = 0; + let curLine = 0; + let columnDeltaInCurLine = 0; + + for (const edit of this.edits) { + const start = edit.range.getStartPosition(); + const end = edit.range.getEndPosition(); + + if (position.isBeforeOrEqual(start)) { + break; + } + + const len = RangeLength.ofText(edit.text); + if (position.isBefore(end)) { + const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); + const endPos = len.addToPosition(startPos); + return rangeFromPositions(startPos, endPos); + } + + lineDelta += len.lineCount - (edit.range.endLineNumber - edit.range.startLineNumber); + + if (len.lineCount === 0) { + if (end.lineNumber !== start.lineNumber) { + columnDeltaInCurLine += len.columnCount - (end.column - 1); + } else { + columnDeltaInCurLine += len.columnCount - (end.column - start.column); + } + } else { + columnDeltaInCurLine = len.columnCount; + } + curLine = end.lineNumber + lineDelta; + } + + return new Position(position.lineNumber + lineDelta, position.column + (position.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); + } + + mapRange(range: Range): Range { + function getStart(p: Position | Range) { + return p instanceof Position ? p : p.getStartPosition(); + } + + function getEnd(p: Position | Range) { + return p instanceof Position ? p : p.getEndPosition(); + } + + const start = getStart(this.mapPosition(range.getStartPosition())); + const end = getEnd(this.mapPosition(range.getEndPosition())); + + return rangeFromPositions(start, end); + } + + // TODO: `doc` is not needed for this! + inverseMapPosition(positionAfterEdit: Position, doc: IText): Position | Range { + const reversed = this.inverse(doc); + return reversed.mapPosition(positionAfterEdit); + } + + inverseMapRange(range: Range, doc: IText): Range { + const reversed = this.inverse(doc); + return reversed.mapRange(range); + } + + apply(text: IText): string { + let result = ''; + let lastEditEnd = new Position(1, 1); + for (const edit of this.edits) { + const editRange = edit.range; + const editStart = editRange.getStartPosition(); + const editEnd = editRange.getEndPosition(); + + const r = rangeFromPositions(lastEditEnd, editStart); + if (!r.isEmpty()) { + result += text.getValue(r); + } + result += edit.text; + lastEditEnd = editEnd; + } + const r = rangeFromPositions(lastEditEnd, text.endPositionExclusive); + if (!r.isEmpty()) { + result += text.getValue(r); + } + return result; + } + + applyToString(str: string): string { + const strText = new StringText(str); + return this.apply(strText); + } + + inverse(doc: IText): TextEdit { + const ranges = this.getNewRanges(); + return new TextEdit(this.edits.map((e, idx) => new SingleTextEdit(ranges[idx], doc.getValue(e.range)))); + } + + getNewRanges(): Range[] { + const newRanges: Range[] = []; + let previousEditEndLineNumber = 0; + let lineOffset = 0; + let columnOffset = 0; + for (const edit of this.edits) { + const textLength = RangeLength.ofText(edit.text); + const newRangeStart = Position.lift({ + lineNumber: edit.range.startLineNumber + lineOffset, + column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) + }); + const newRange = textLength.createRange(newRangeStart); + newRanges.push(newRange); + lineOffset = newRange.endLineNumber - edit.range.endLineNumber; + columnOffset = newRange.endColumn - edit.range.endColumn; + previousEditEndLineNumber = edit.range.endLineNumber; + } + return newRanges; + } +} + +export class SingleTextEdit { + constructor( + public readonly range: Range, + public readonly text: string, + ) { + } + + static equals(first: SingleTextEdit, second: SingleTextEdit) { + return first.range.equalsRange(second.range) && first.text === second.text; + } +} + +function rangeFromPositions(start: Position, end: Position): Range { + if (!start.isBeforeOrEqual(end)) { + throw new BugIndicatingError('start must be before end'); + } + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); +} + +export interface IText { + getValue(range: Range): string; + readonly endPositionExclusive: Position; +} + +export class LineBasedText implements IText { + constructor( + private readonly _getLineContent: (lineNumber: number) => string, + private readonly _lineCount: number, + ) { } + + getValue(range: Range): string { + if (range.startLineNumber === range.endLineNumber) { + return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); + } + let result = this._getLineContent(range.startLineNumber).substring(range.startColumn - 1); + for (let i = range.startLineNumber + 1; i < range.endLineNumber; i++) { + result += '\n' + this._getLineContent(i); + } + result += '\n' + this._getLineContent(range.endLineNumber).substring(0, range.endColumn - 1); + return result; + } + + get endPositionExclusive(): Position { + const lastLine = this._getLineContent(this._lineCount); + return new Position(this._lineCount, lastLine.length + 1); + } +} + +export class StringText implements IText { + private readonly _t = new PositionOffsetTransformer(this.str); + + constructor(private readonly str: string) { } + + getValue(range: Range): string { + return this._t.getOffsetRange(range).substring(this.str); + } + + get endPositionExclusive(): Position { + return this._t.getPosition(this.str.length); + } +} diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 7117b8240af..10002234dde 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1256,7 +1256,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati ); } - private _validateEditOperations(rawOperations: model.IIdentifiedSingleEditOperation[]): model.ValidAnnotatedEditOperation[] { + private _validateEditOperations(rawOperations: readonly model.IIdentifiedSingleEditOperation[]): model.ValidAnnotatedEditOperation[] { const result: model.ValidAnnotatedEditOperation[] = []; for (let i = 0, len = rawOperations.length; i < len; i++) { result[i] = this._validateEditOperation(rawOperations[i]); @@ -1406,10 +1406,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } } - public applyEdits(operations: model.IIdentifiedSingleEditOperation[]): void; - public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void; - public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[]; - public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] { + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[]): void; + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void; + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[]; + public applyEdits(rawOperations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); diff --git a/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts new file mode 100644 index 00000000000..40433478d4b --- /dev/null +++ b/src/vs/editor/common/model/textModelText.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IText } from 'vs/editor/common/core/textEdit'; +import { ITextModel } from 'vs/editor/common/model'; + +export class TextModelText implements IText { + constructor(private readonly _textModel: ITextModel) { } + + getValue(range: Range): string { + return this._textModel.getValueInRange(range); + } + + get endPositionExclusive(): Position { + const lastLineNumber = this._textModel.getLineCount(); + const lastLineLen = this._textModel.getLineLength(lastLineNumber); + return new Position(lastLineNumber, lastLineLen + 1); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts index 42d0404984f..21abd0b7093 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts @@ -5,8 +5,10 @@ import { equals } from 'vs/base/common/arrays'; import { splitLines } from 'vs/base/common/strings'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ColumnRange, applyEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { ColumnRange } from 'vs/editor/contrib/inlineCompletions/browser/utils'; export class GhostText { constructor( @@ -25,13 +27,12 @@ export class GhostText { * Only used for testing/debugging. */ render(documentText: string, debug: boolean = false): string { - const l = this.lineNumber; - return applyEdits(documentText, [ - ...this.parts.map(p => ({ - range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column }, - text: debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n') - })), - ]); + return new TextEdit([ + ...this.parts.map(p => new SingleTextEdit( + Range.fromPositions(new Position(this.lineNumber, p.column)), + debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n') + )), + ]).applyToString(documentText); } renderForScreenReader(lineText: string): string { @@ -41,12 +42,12 @@ export class GhostText { const lastPart = this.parts[this.parts.length - 1]; const cappedLineText = lineText.substr(0, lastPart.column - 1); - const text = applyEdits(cappedLineText, - this.parts.map(p => ({ - range: { startLineNumber: 1, endLineNumber: 1, startColumn: p.column, endColumn: p.column }, - text: p.lines.join('\n') - })) - ); + const text = new TextEdit([ + ...this.parts.map(p => new SingleTextEdit( + Range.fromPositions(new Position(1, p.column)), + p.lines.join('\n') + )), + ]).applyToString(cappedLineText); return text.substring(this.parts[0].column - 1); } @@ -106,14 +107,14 @@ export class GhostTextReplacement { const replaceRange = this.columnRange.toRange(this.lineNumber); if (debug) { - return applyEdits(documentText, [ - { range: Range.fromPositions(replaceRange.getStartPosition()), text: `(` }, - { range: Range.fromPositions(replaceRange.getEndPosition()), text: `)[${this.newLines.join('\n')}]` } - ]); + return new TextEdit([ + new SingleTextEdit(Range.fromPositions(replaceRange.getStartPosition()), '('), + new SingleTextEdit(Range.fromPositions(replaceRange.getEndPosition()), `)[${this.newLines.join('\n')}]`), + ]).applyToString(documentText); } else { - return applyEdits(documentText, [ - { range: replaceRange, text: this.newLines.join('\n') } - ]); + return new TextEdit([ + new SingleTextEdit(replaceRange, this.newLines.join('\n')), + ]).applyToString(documentText); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 8bc30856f20..57357fd47e1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -21,12 +21,14 @@ import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; -import { addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { addPositions, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { singleTextEditAugments, computeGhostText, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; export enum VersionIdChangeReason { Undo, @@ -213,7 +215,7 @@ export class InlineCompletionsModel extends Disposable { const suggestItem = this.selectedSuggestItem.read(reader); if (suggestItem) { - const suggestCompletionEdit = suggestItem.toSingleTextEdit().removeCommonPrefix(model); + const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.toSingleTextEdit(), model); const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); @@ -226,7 +228,7 @@ export class InlineCompletionsModel extends Disposable { const positions = this._positions.read(reader); const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; const ghostTexts = edits - .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], fullEditPreviewLength)) + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) .filter(isDefined); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); return { edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; @@ -240,7 +242,7 @@ export class InlineCompletionsModel extends Disposable { const positions = this._positions.read(reader); const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; const ghostTexts = edits - .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], 0)) + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) .filter(isDefined); if (!ghostTexts[0]) { return undefined; } return { edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; @@ -256,8 +258,8 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { let r = completion.toSingleTextEdit(reader); - r = r.removeCommonPrefix(model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); - return r.augments(suggestCompletion) ? { completion, edit: r } : undefined; + r = singleTextRemoveCommonPrefix(r, model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); + return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined; }); return augmentedCompletion; @@ -442,7 +444,7 @@ export class InlineCompletionsModel extends Disposable { } if (completion.source.provider.handlePartialAccept) { - const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), addPositions(ghostTextPos, lengthOfText(partialGhostTextVal))); + const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), RangeLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); // This assumes that the inline completion and the model use the same EOL style. const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); completion.source.provider.handlePartialAccept( @@ -460,7 +462,7 @@ export class InlineCompletionsModel extends Disposable { } public handleSuggestAccepted(item: SuggestItemInfo) { - const itemEdit = item.toSingleTextEdit().removeCommonPrefix(this.textModel); + const itemEdit = singleTextRemoveCommonPrefix(item.toSingleTextEdit(), this.textModel); const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); if (!augmentedCompletion) { return; } @@ -519,7 +521,8 @@ function substringPos(text: string, pos: Position): string { function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); - const sortedNewRanges = getNewRanges(sortPerm.apply(edits)); + const edit = new TextEdit(sortPerm.apply(edits)); + const sortedNewRanges = edit.getNewRanges(); const newRanges = sortPerm.inverse().apply(sortedNewRanges); return newRanges.map(range => range.getEndPosition()); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 3e38e9e8161..94a8b33d477 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -15,7 +15,8 @@ import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; +import { singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class InlineCompletionsSource extends Disposable { private readonly _updateOperation = this._register(new MutableDisposable()); @@ -282,7 +283,7 @@ export class InlineCompletionWithUpdatedRange { } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { - const minimizedReplacement = this._toFilterTextReplacement(reader).removeCommonPrefix(model); + const minimizedReplacement = singleTextRemoveCommonPrefix(this._toFilterTextReplacement(reader), model); if ( !this._isValid diff --git a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 9d91e0ade1e..28052040c32 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts @@ -17,7 +17,7 @@ import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionPro import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts index 8517f24ec85..d38d1e41f57 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts @@ -7,147 +7,136 @@ import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff'; import { commonPrefixLength, getLeadingWhitespace } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; -import { addPositions, lengthOfText } from 'vs/editor/contrib/inlineCompletions/browser/utils'; -export class SingleTextEdit { - constructor( - public readonly range: Range, - public readonly text: string - ) { +export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextModel, validModelRange?: Range): SingleTextEdit { + const modelRange = validModelRange ? edit.range.intersectRanges(validModelRange) : edit.range; + if (!modelRange) { + return edit; + } + const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); + const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); + const start = RangeLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); + const text = edit.text.substring(commonPrefixLen); + const range = Range.fromPositions(start, edit.range.getEndPosition()); + return new SingleTextEdit(range, text); +} + +export function singleTextEditAugments(edit: SingleTextEdit, base: SingleTextEdit): boolean { + // The augmented completion must replace the base range, but can replace even more + return edit.text.startsWith(base.text) && rangeExtends(edit.range, base.range); +} + +/** + * @param previewSuffixLength Sets where to split `inlineCompletion.text`. + * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. +*/ +export function computeGhostText( + edit: SingleTextEdit, + model: ITextModel, + mode: 'prefix' | 'subword' | 'subwordSmart', + cursorPosition?: Position, + previewSuffixLength = 0 +): GhostText | undefined { + let e = singleTextRemoveCommonPrefix(edit, model); + + if (e.range.endLineNumber !== e.range.startLineNumber) { + // This edit might span multiple lines, but the first lines must be a common prefix. + return undefined; } - static equals(first: SingleTextEdit, second: SingleTextEdit) { - return first.range.equalsRange(second.range) && first.text === second.text; + const sourceLine = model.getLineContent(e.range.startLineNumber); + const sourceIndentationLength = getLeadingWhitespace(sourceLine).length; + const suggestionTouchesIndentation = e.range.startColumn - 1 <= sourceIndentationLength; + if (suggestionTouchesIndentation) { + // source: ··········[······abc] + // ^^^^^^^^^ inlineCompletion.range + // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength + // ^^^^^^ replacedIndentation.length + // ^^^ rangeThatDoesNotReplaceIndentation + + // inlineCompletion.text: '··foo' + // ^^ suggestionAddedIndentationLength + + const suggestionAddedIndentationLength = getLeadingWhitespace(e.text).length; + + const replacedIndentation = sourceLine.substring(e.range.startColumn - 1, sourceIndentationLength); + + const [startPosition, endPosition] = [e.range.getStartPosition(), e.range.getEndPosition()]; + const newStartPosition = + startPosition.column + replacedIndentation.length <= endPosition.column + ? startPosition.delta(0, replacedIndentation.length) + : endPosition; + const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition); + + const suggestionWithoutIndentationChange = + e.text.startsWith(replacedIndentation) + // Adds more indentation without changing existing indentation: We can add ghost text for this + ? e.text.substring(replacedIndentation.length) + // Changes or removes existing indentation. Only add ghost text for the non-indentation part. + : e.text.substring(suggestionAddedIndentationLength); + + e = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); } - removeCommonPrefix(model: ITextModel, validModelRange?: Range): SingleTextEdit { - const modelRange = validModelRange ? this.range.intersectRanges(validModelRange) : this.range; - if (!modelRange) { - return this; + // This is a single line string + const valueToBeReplaced = model.getValueInRange(e.range); + + const changes = cachingDiff(valueToBeReplaced, e.text); + + if (!changes) { + // No ghost text in case the diff would be too slow to compute + return undefined; + } + + const lineNumber = e.range.startLineNumber; + + const parts = new Array(); + + if (mode === 'prefix') { + const filteredChanges = changes.filter(c => c.originalLength === 0); + if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { + // Prefixes only have a single change. + return undefined; } - const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); - const commonPrefixLen = commonPrefixLength(valueToReplace, this.text); - const start = addPositions(this.range.getStartPosition(), lengthOfText(valueToReplace.substring(0, commonPrefixLen))); - const text = this.text.substring(commonPrefixLen); - const range = Range.fromPositions(start, this.range.getEndPosition()); - return new SingleTextEdit(range, text); } - augments(base: SingleTextEdit): boolean { - // The augmented completion must replace the base range, but can replace even more - return this.text.startsWith(base.text) && rangeExtends(this.range, base.range); - } + const previewStartInCompletionText = e.text.length - previewSuffixLength; - /** - * @param previewSuffixLength Sets where to split `inlineCompletion.text`. - * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. - */ - computeGhostText( - model: ITextModel, - mode: 'prefix' | 'subword' | 'subwordSmart', - cursorPosition?: Position, - previewSuffixLength = 0 - ): GhostText | undefined { - let edit = this.removeCommonPrefix(model); + for (const c of changes) { + const insertColumn = e.range.startColumn + c.originalStart + c.originalLength; - if (edit.range.endLineNumber !== edit.range.startLineNumber) { - // This edit might span multiple lines, but the first lines must be a common prefix. + if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === e.range.startLineNumber && insertColumn < cursorPosition.column) { + // No ghost text before cursor return undefined; } - const sourceLine = model.getLineContent(edit.range.startLineNumber); - const sourceIndentationLength = getLeadingWhitespace(sourceLine).length; - - const suggestionTouchesIndentation = edit.range.startColumn - 1 <= sourceIndentationLength; - if (suggestionTouchesIndentation) { - // source: ··········[······abc] - // ^^^^^^^^^ inlineCompletion.range - // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength - // ^^^^^^ replacedIndentation.length - // ^^^ rangeThatDoesNotReplaceIndentation - - // inlineCompletion.text: '··foo' - // ^^ suggestionAddedIndentationLength - - const suggestionAddedIndentationLength = getLeadingWhitespace(edit.text).length; - - const replacedIndentation = sourceLine.substring(edit.range.startColumn - 1, sourceIndentationLength); - - const [startPosition, endPosition] = [edit.range.getStartPosition(), edit.range.getEndPosition()]; - const newStartPosition = - startPosition.column + replacedIndentation.length <= endPosition.column - ? startPosition.delta(0, replacedIndentation.length) - : endPosition; - const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition); - - const suggestionWithoutIndentationChange = - edit.text.startsWith(replacedIndentation) - // Adds more indentation without changing existing indentation: We can add ghost text for this - ? edit.text.substring(replacedIndentation.length) - // Changes or removes existing indentation. Only add ghost text for the non-indentation part. - : edit.text.substring(suggestionAddedIndentationLength); - - edit = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); - } - - // This is a single line string - const valueToBeReplaced = model.getValueInRange(edit.range); - - const changes = cachingDiff(valueToBeReplaced, edit.text); - - if (!changes) { - // No ghost text in case the diff would be too slow to compute + if (c.originalLength > 0) { return undefined; } - const lineNumber = edit.range.startLineNumber; - - const parts = new Array(); - - if (mode === 'prefix') { - const filteredChanges = changes.filter(c => c.originalLength === 0); - if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { - // Prefixes only have a single change. - return undefined; - } + if (c.modifiedLength === 0) { + continue; } - const previewStartInCompletionText = edit.text.length - previewSuffixLength; + const modifiedEnd = c.modifiedStart + c.modifiedLength; + const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText)); + const nonPreviewText = e.text.substring(c.modifiedStart, nonPreviewTextEnd); + const italicText = e.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd)); - for (const c of changes) { - const insertColumn = edit.range.startColumn + c.originalStart + c.originalLength; - - if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === edit.range.startLineNumber && insertColumn < cursorPosition.column) { - // No ghost text before cursor - return undefined; - } - - if (c.originalLength > 0) { - return undefined; - } - - if (c.modifiedLength === 0) { - continue; - } - - const modifiedEnd = c.modifiedStart + c.modifiedLength; - const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText)); - const nonPreviewText = edit.text.substring(c.modifiedStart, nonPreviewTextEnd); - const italicText = edit.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd)); - - if (nonPreviewText.length > 0) { - parts.push(new GhostTextPart(insertColumn, nonPreviewText, false)); - } - if (italicText.length > 0) { - parts.push(new GhostTextPart(insertColumn, italicText, true)); - } + if (nonPreviewText.length > 0) { + parts.push(new GhostTextPart(insertColumn, nonPreviewText, false)); + } + if (italicText.length > 0) { + parts.push(new GhostTextPart(insertColumn, italicText, true)); } - - return new GhostText(lineNumber, parts); } + + return new GhostText(lineNumber, parts); } function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 90d53b26c8f..5f98b45033a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -14,10 +14,11 @@ import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { ITextModel } from 'vs/editor/common/model'; import { compareBy, numberComparator } from 'vs/base/common/arrays'; import { findFirstMaxBy } from 'vs/base/common/arraysFind'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class SuggestWidgetAdaptor extends Disposable { private isSuggestWidgetVisible: boolean = false; @@ -66,7 +67,8 @@ export class SuggestWidgetAdaptor extends Disposable { return -1; } - const itemToPreselect = this.suggestControllerPreselector()?.removeCommonPrefix(textModel); + const i = this.suggestControllerPreselector(); + const itemToPreselect = i ? singleTextRemoveCommonPrefix(i, textModel) : undefined; if (!itemToPreselect) { return -1; } @@ -75,8 +77,8 @@ export class SuggestWidgetAdaptor extends Disposable { const candidates = suggestItems .map((suggestItem, index) => { const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed); - const suggestItemTextEdit = suggestItemInfo.toSingleTextEdit().removeCommonPrefix(textModel); - const valid = itemToPreselect.augments(suggestItemTextEdit); + const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.toSingleTextEdit(), textModel); + const valid = singleTextEditAugments(itemToPreselect, suggestItemTextEdit); return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem }; }) .filter(item => item && item.valid && item.prefixLength > 0); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 0dc96bb3945..20236aade3b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -3,54 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Permutation, compareBy } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; - -export function applyEdits(text: string, edits: { range: IRange; text: string }[]): string { - const transformer = new PositionOffsetTransformer(text); - const offsetEdits = edits.map(e => { - const range = Range.lift(e.range); - return ({ - startOffset: transformer.getOffset(range.getStartPosition()), - endOffset: transformer.getOffset(range.getEndPosition()), - text: e.text - }); - }); - - offsetEdits.sort((a, b) => b.startOffset - a.startOffset); - - for (const edit of offsetEdits) { - text = text.substring(0, edit.startOffset) + edit.text + text.substring(edit.endOffset); - } - - return text; -} - -class PositionOffsetTransformer { - private readonly lineStartOffsetByLineIdx: number[]; - - constructor(text: string) { - this.lineStartOffsetByLineIdx = []; - this.lineStartOffsetByLineIdx.push(0); - for (let i = 0; i < text.length; i++) { - if (text.charAt(i) === '\n') { - this.lineStartOffsetByLineIdx.push(i + 1); - } - } - } - - getOffset(position: Position): number { - return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; - } -} const array: ReadonlyArray = []; export function getReadonlyEmptyArray(): readonly T[] { @@ -99,65 +58,3 @@ export function addPositions(pos1: Position, pos2: Position): Position { export function subtractPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber - pos2.lineNumber + 1, pos1.lineNumber - pos2.lineNumber === 0 ? pos1.column - pos2.column + 1 : pos1.column); } - -export function lengthOfText(text: string): Position { - let line = 1; - let column = 1; - for (const c of text) { - if (c === '\n') { - line++; - column = 1; - } else { - column++; - } - } - return new Position(line, column); -} - -/** - * Given some text edits, this function finds the new ranges of the editted text post application of all edits. - * Assumes that the edit ranges are disjoint and they are sorted in the order of the ranges - * @param edits edits applied - * @returns new ranges post edits for every edit - */ -export function getNewRanges(edits: ISingleEditOperation[]): Range[] { - const newRanges: Range[] = []; - let previousEditEndLineNumber = 0; - let lineOffset = 0; - let columnOffset = 0; - - for (const edit of edits) { - const text = edit.text ?? ''; - const textLength = lengthOfText(text); - const newRangeStart = Position.lift({ - lineNumber: edit.range.startLineNumber + lineOffset, - column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) - }); - const newRangeEnd = addPositions( - newRangeStart, - textLength - ); - newRanges.push(Range.fromPositions(newRangeStart, newRangeEnd)); - lineOffset += textLength.lineNumber - edit.range.endLineNumber + edit.range.startLineNumber - 1; - columnOffset = newRangeEnd.column - edit.range.endColumn; - previousEditEndLineNumber = edit.range.endLineNumber; - } - return newRanges; -} - -/** - * Given a text model and edits, this function finds the inverse text edits - * @param model model on which to apply the edits - * @param edits edits applied - * @returns inverse edits - */ -export function inverseEdits(model: TextModel, edits: ISingleEditOperation[]): ISingleEditOperation[] { - const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); - const sortedRanges = getNewRanges(sortPerm.apply(edits)); - const newRanges = sortPerm.inverse().apply(sortedRanges); - const inverseEdits: ISingleEditOperation[] = []; - for (let i = 0; i < edits.length; i++) { - inverseEdits.push({ range: newRanges[i], text: model.getValueInRange(edits[i].range) }); - } - return inverseEdits; -} diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 67c56bb3945..648f5940596 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { Range } from 'vs/editor/common/core/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 4c846025949..e160c3daa01 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -15,13 +15,14 @@ import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeatu import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/browser/utils'; import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { Selection } from 'vs/editor/common/core/selection'; +import { computeGhostText } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -37,7 +38,7 @@ suite('Inline Completions', () => { const options = ['prefix', 'subword'] as const; const result = {} as any; for (const option of options) { - result[option] = new SingleTextEdit(range, suggestion).computeGhostText(tempModel, option)?.render(cleanedText, true); + result[option] = computeGhostText(new SingleTextEdit(range, suggestion), tempModel, option)?.render(cleanedText, true); } tempModel.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts deleted file mode 100644 index 16c918fbe18..00000000000 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MersenneTwister, getRandomEditInfos, toEdit, } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; -import { inverseEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils'; -import { generateRandomMultilineString } from 'vs/editor/contrib/inlineCompletions/test/browser/utils'; - -suite('getNewRanges', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - for (let seed = 0; seed < 20; seed++) { - test(`test ${seed}`, () => { - const rng = new MersenneTwister(seed); - const randomText = generateRandomMultilineString(rng, 10); - const model = createTextModel(randomText); - - const edits = getRandomEditInfos(model, rng.nextIntRange(1, 4), rng, true).map(e => toEdit(e)); - const invEdits = inverseEdits(model, edits); - - model.applyEdits(edits); - model.applyEdits(invEdits); - - assert.deepStrictEqual(model.getValue(), randomText); - model.dispose(); - }); - } - -}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 49e09d4e4a7..50250883440 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -133,23 +133,3 @@ export class GhostTextContext extends Disposable { } } -export function generateRandomMultilineString(rng: MersenneTwister, numberOfLines: number, maximumLengthOfLines: number = 20): string { - let randomText: string = ''; - for (let i = 0; i < numberOfLines; i++) { - const lengthOfLine = rng.nextIntRange(0, maximumLengthOfLines + 1); - randomText += generateRandomSimpleString(rng, lengthOfLine) + '\n'; - } - return randomText; -} - -function generateRandomSimpleString(rng: MersenneTwister, stringLength: number): string { - const possibleCharacters: string = ' abcdefghijklmnopqrstuvwxyz0123456789'; - let randomText: string = ''; - for (let i = 0; i < stringLength; i++) { - const characterIndex = rng.nextIntRange(0, possibleCharacters.length); - randomText += possibleCharacters.charAt(characterIndex); - - } - return randomText; -} - diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts new file mode 100644 index 00000000000..39aead0a848 --- /dev/null +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; + +suite('PositionOffsetTransformer', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const str = '123456\nabcdef\nghijkl\nmnopqr'; + + const t = new PositionOffsetTransformer(str); + test('getPosition', () => { + assert.deepStrictEqual( + new OffsetRange(0, str.length + 2).map(i => t.getPosition(i).toString()), + [ + "(1,1)", + "(1,2)", + "(1,3)", + "(1,4)", + "(1,5)", + "(1,6)", + "(1,7)", + "(2,1)", + "(2,2)", + "(2,3)", + "(2,4)", + "(2,5)", + "(2,6)", + "(2,7)", + "(3,1)", + "(3,2)", + "(3,3)", + "(3,4)", + "(3,5)", + "(3,6)", + "(3,7)", + "(4,1)", + "(4,2)", + "(4,3)", + "(4,4)", + "(4,5)", + "(4,6)", + "(4,7)", + "(4,8)" + ] + ); + }); + + test('getOffset', () => { + for (let i = 0; i < str.length + 2; i++) { + assert.strictEqual(t.getOffset(t.getPosition(i)), i); + } + }); +}); diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts new file mode 100644 index 00000000000..1e9c342ebcd --- /dev/null +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { MersenneTwister, getRandomEditInfos, toEdit, } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; +import { TextEdit } from 'vs/editor/common/core/textEdit'; +import { TextModelText } from 'vs/editor/common/model/textModelText'; + +suite('TextEdit', () => { + suite('inverse', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function runTest(seed: number): void { + const rng = new MersenneTwister(seed); + const randomText = generateRandomMultilineString(rng, 10); + const model = createTextModel(randomText); + + const edits = new TextEdit(getRandomEditInfos(model, rng.nextIntRange(1, 4), rng, true).map(e => toEdit(e))); + const invEdits = edits.inverse(new TextModelText(model)); + + model.applyEdits(edits.edits); + model.applyEdits(invEdits.edits); + + assert.deepStrictEqual(model.getValue(), randomText); + model.dispose(); + } + + test.skip('brute-force', () => { + for (let i = 0; i < 10_000; i++) { + runTest(i); + } + }); + + for (let seed = 0; seed < 20; seed++) { + test(`test ${seed}`, () => runTest(seed)); + } + }); +}); + +function generateRandomMultilineString(rng: MersenneTwister, numberOfLines: number, maximumLengthOfLines: number = 20): string { + let randomText: string = ''; + for (let i = 0; i < numberOfLines; i++) { + const lengthOfLine = rng.nextIntRange(0, maximumLengthOfLines + 1); + randomText += generateRandomSimpleString(rng, lengthOfLine) + '\n'; + } + return randomText; +} + +function generateRandomSimpleString(rng: MersenneTwister, stringLength: number): string { + const possibleCharacters: string = ' abcdefghijklmnopqrstuvwxyz0123456789'; + let randomText: string = ''; + for (let i = 0; i < stringLength; i++) { + const characterIndex = rng.nextIntRange(0, possibleCharacters.length); + randomText += possibleCharacters.charAt(characterIndex); + + } + return randomText; +} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 611ba267d5b..05f2063baf1 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; import { lengthAdd, lengthToObj, lengthToPosition, positionToLength, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; @@ -14,7 +14,6 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; suite('combineTextEditInfos', () => { - ensureNoDisposablesAreLeakedInTestSuite(); for (let seed = 0; seed < 50; seed++) { @@ -79,7 +78,7 @@ function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Mers return new TextEditInfo(positionToLength(textModel.getPositionAt(offsetStart)), positionToLength(textModel.getPositionAt(offsetEnd)), toLength(lineCount, columnCount)); } -export function toEdit(editInfo: TextEditInfo): ISingleEditOperation { +export function toEdit(editInfo: TextEditInfo): SingleTextEdit { const l = lengthToObj(editInfo.newLength); let text = ''; From af2b2ecbbd0136bde36fc13544aae7c9f22d0b49 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 8 Mar 2024 15:14:46 +0100 Subject: [PATCH 074/141] Introduces test utility "Random" for deterministic random-like data generation --- src/vs/editor/common/core/positionToOffset.ts | 12 +- src/vs/editor/common/core/rangeLength.ts | 4 + src/vs/editor/common/core/textEdit.ts | 82 +++++++++---- src/vs/editor/common/model/textModelText.ts | 16 +-- .../inlineCompletions/test/browser/utils.ts | 1 - src/vs/editor/test/common/core/random.ts | 113 ++++++++++++++++++ .../editor/test/common/core/textEdit.test.ts | 45 ++----- .../combineTextEditInfos.test.ts | 60 ++-------- 8 files changed, 214 insertions(+), 119 deletions(-) create mode 100644 src/vs/editor/test/common/core/random.ts diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts index c4abee8a332..e181b90173c 100644 --- a/src/vs/editor/common/core/positionToOffset.ts +++ b/src/vs/editor/common/core/positionToOffset.ts @@ -7,11 +7,12 @@ import { findLastIdxMonotonous } from 'vs/base/common/arraysFind'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; export class PositionOffsetTransformer { private readonly lineStartOffsetByLineIdx: number[]; - constructor(text: string) { + constructor(public readonly text: string) { this.lineStartOffsetByLineIdx = []; this.lineStartOffsetByLineIdx.push(0); for (let i = 0; i < text.length; i++) { @@ -45,4 +46,13 @@ export class PositionOffsetTransformer { this.getPosition(offsetRange.endExclusive) ); } + + getTextLength(offsetRange: OffsetRange): RangeLength { + return RangeLength.ofRange(this.getRange(offsetRange)); + } + + get textLength(): RangeLength { + const lineIdx = this.lineStartOffsetByLineIdx.length - 1; + return new RangeLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]); + } } diff --git a/src/vs/editor/common/core/rangeLength.ts b/src/vs/editor/common/core/rangeLength.ts index 90c47e092c9..d01fca41c1f 100644 --- a/src/vs/editor/common/core/rangeLength.ts +++ b/src/vs/editor/common/core/rangeLength.ts @@ -98,6 +98,10 @@ export class RangeLength { } } + public toRange(): Range { + return new Range(1, 1, this.lineCount + 1, this.columnCount + 1); + } + public addToPosition(position: Position): Position { if (this.lineCount === 0) { return new Position(position.lineNumber, position.column + this.columnCount); diff --git a/src/vs/editor/common/core/textEdit.ts b/src/vs/editor/common/core/textEdit.ts index 42e94c281d7..ade6f7bf16a 100644 --- a/src/vs/editor/common/core/textEdit.ts +++ b/src/vs/editor/common/core/textEdit.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; +import { assert, assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Position } from 'vs/editor/common/core/position'; import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; @@ -15,6 +15,22 @@ export class TextEdit { assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); } + /** + * Joins touching edits and removes empty edits. + */ + normalize(): TextEdit { + const edits: SingleTextEdit[] = []; + for (const edit of this.edits) { + if (edits.length > 0 && edits[edits.length - 1].range.getEndPosition().equals(edit.range.getStartPosition())) { + const last = edits[edits.length - 1]; + edits[edits.length - 1] = new SingleTextEdit(last.range.plusRange(edit.range), last.text + edit.text); + } else if (!edit.isEmpty) { + edits.push(edit); + } + } + return new TextEdit(edits); + } + mapPosition(position: Position): Position | Range { let lineDelta = 0; let curLine = 0; @@ -68,17 +84,17 @@ export class TextEdit { } // TODO: `doc` is not needed for this! - inverseMapPosition(positionAfterEdit: Position, doc: IText): Position | Range { + inverseMapPosition(positionAfterEdit: Position, doc: AbstractText): Position | Range { const reversed = this.inverse(doc); return reversed.mapPosition(positionAfterEdit); } - inverseMapRange(range: Range, doc: IText): Range { + inverseMapRange(range: Range, doc: AbstractText): Range { const reversed = this.inverse(doc); return reversed.mapRange(range); } - apply(text: IText): string { + apply(text: AbstractText): string { let result = ''; let lastEditEnd = new Position(1, 1); for (const edit of this.edits) { @@ -88,14 +104,14 @@ export class TextEdit { const r = rangeFromPositions(lastEditEnd, editStart); if (!r.isEmpty()) { - result += text.getValue(r); + result += text.getValueOfRange(r); } result += edit.text; lastEditEnd = editEnd; } const r = rangeFromPositions(lastEditEnd, text.endPositionExclusive); if (!r.isEmpty()) { - result += text.getValue(r); + result += text.getValueOfRange(r); } return result; } @@ -105,9 +121,9 @@ export class TextEdit { return this.apply(strText); } - inverse(doc: IText): TextEdit { + inverse(doc: AbstractText): TextEdit { const ranges = this.getNewRanges(); - return new TextEdit(this.edits.map((e, idx) => new SingleTextEdit(ranges[idx], doc.getValue(e.range)))); + return new TextEdit(this.edits.map((e, idx) => new SingleTextEdit(ranges[idx], doc.getValueOfRange(e.range)))); } getNewRanges(): Range[] { @@ -138,6 +154,10 @@ export class SingleTextEdit { ) { } + get isEmpty(): boolean { + return this.range.isEmpty() && this.text.length === 0; + } + static equals(first: SingleTextEdit, second: SingleTextEdit) { return first.range.equalsRange(second.range) && first.text === second.text; } @@ -150,18 +170,30 @@ function rangeFromPositions(start: Position, end: Position): Range { return new Range(start.lineNumber, start.column, end.lineNumber, end.column); } -export interface IText { - getValue(range: Range): string; - readonly endPositionExclusive: Position; +export abstract class AbstractText { + abstract getValueOfRange(range: Range): string; + abstract readonly length: RangeLength; + + get endPositionExclusive(): Position { + return this.length.addToPosition(new Position(1, 1)); + } + + getValue() { + return this.getValueOfRange(this.length.toRange()); + } } -export class LineBasedText implements IText { +export class LineBasedText extends AbstractText { constructor( private readonly _getLineContent: (lineNumber: number) => string, private readonly _lineCount: number, - ) { } + ) { + assert(_lineCount >= 1); - getValue(range: Range): string { + super(); + } + + getValueOfRange(range: Range): string { if (range.startLineNumber === range.endLineNumber) { return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); } @@ -173,22 +205,24 @@ export class LineBasedText implements IText { return result; } - get endPositionExclusive(): Position { + get length(): RangeLength { const lastLine = this._getLineContent(this._lineCount); - return new Position(this._lineCount, lastLine.length + 1); + return new RangeLength(this._lineCount - 1, lastLine.length); } } -export class StringText implements IText { - private readonly _t = new PositionOffsetTransformer(this.str); +export class StringText extends AbstractText { + private readonly _t = new PositionOffsetTransformer(this.value); - constructor(private readonly str: string) { } - - getValue(range: Range): string { - return this._t.getOffsetRange(range).substring(this.str); + constructor(public readonly value: string) { + super(); } - get endPositionExclusive(): Position { - return this._t.getPosition(this.str.length); + getValueOfRange(range: Range): string { + return this._t.getOffsetRange(range).substring(this.value); + } + + get length(): RangeLength { + return this._t.textLength; } } diff --git a/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts index 40433478d4b..36f73a63d67 100644 --- a/src/vs/editor/common/model/textModelText.ts +++ b/src/vs/editor/common/model/textModelText.ts @@ -3,21 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IText } from 'vs/editor/common/core/textEdit'; +import { AbstractText } from 'vs/editor/common/core/textEdit'; +import { RangeLength } from 'vs/editor/common/core/rangeLength'; import { ITextModel } from 'vs/editor/common/model'; -export class TextModelText implements IText { - constructor(private readonly _textModel: ITextModel) { } +export class TextModelText extends AbstractText { + constructor(private readonly _textModel: ITextModel) { + super(); + } - getValue(range: Range): string { + getValueOfRange(range: Range): string { return this._textModel.getValueInRange(range); } - get endPositionExclusive(): Position { + get length(): RangeLength { const lastLineNumber = this._textModel.getLineCount(); const lastLineLen = this._textModel.getLineLength(lastLineNumber); - return new Position(lastLineNumber, lastLineLen + 1); + return new RangeLength(lastLineNumber - 1, lastLineLen); } } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 50250883440..11c24b0b0e6 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -13,7 +13,6 @@ import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } import { ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { autorun } from 'vs/base/common/observable'; -import { MersenneTwister } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; diff --git a/src/vs/editor/test/common/core/random.ts b/src/vs/editor/test/common/core/random.ts new file mode 100644 index 00000000000..d48f4173f82 --- /dev/null +++ b/src/vs/editor/test/common/core/random.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { numberComparator } from 'vs/base/common/arrays'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Position } from 'vs/editor/common/core/position'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; +import { Range } from 'vs/editor/common/core/range'; +import { AbstractText, SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; + +export abstract class Random { + public static basicAlphabet: string = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + public static basicAlphabetMultiline: string = ' \n\n\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + public static create(seed: number): Random { + return new MersenneTwister(seed); + } + + public abstract nextIntRange(start: number, endExclusive: number): number; + + public nextString(length: number, alphabet = Random.basicAlphabet): string { + let randomText: string = ''; + for (let i = 0; i < length; i++) { + const characterIndex = this.nextIntRange(0, alphabet.length); + randomText += alphabet.charAt(characterIndex); + } + return randomText; + } + + public nextMultiLineString(lineCount: number, lineLengthRange: OffsetRange, alphabet = Random.basicAlphabet): string { + const lines: string[] = []; + for (let i = 0; i < lineCount; i++) { + const lineLength = this.nextIntRange(lineLengthRange.start, lineLengthRange.endExclusive); + lines.push(this.nextString(lineLength, alphabet)); + } + return lines.join('\n'); + } + + public nextConsecutivePositions(source: AbstractText, count: number): Position[] { + const t = new PositionOffsetTransformer(source.getValue()); + const offsets = OffsetRange.ofLength(count).map(() => this.nextIntRange(0, t.text.length)); + offsets.sort(numberComparator); + return offsets.map(offset => t.getPosition(offset)); + } + + public nextRange(source: AbstractText): Range { + const [start, end] = this.nextConsecutivePositions(source, 2); + return Range.fromPositions(start, end); + } + + public nextTextEdit(target: AbstractText, singleTextEditCount: number): TextEdit { + const singleTextEdits: SingleTextEdit[] = []; + + const positions = this.nextConsecutivePositions(target, singleTextEditCount * 2); + + for (let i = 0; i < singleTextEditCount; i++) { + const start = positions[i * 2]; + const end = positions[i * 2 + 1]; + const newText = this.nextString(end.column - start.column, Random.basicAlphabetMultiline); + singleTextEdits.push(new SingleTextEdit(Range.fromPositions(start, end), newText)); + } + + return new TextEdit(singleTextEdits).normalize(); + } +} + +class MersenneTwister extends Random { + private readonly mt = new Array(624); + private index = 0; + + constructor(seed: number) { + super(); + + this.mt[0] = seed >>> 0; + for (let i = 1; i < 624; i++) { + const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); + this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0; + } + } + + private _nextInt() { + if (this.index === 0) { + this.generateNumbers(); + } + + let y = this.mt[this.index]; + y = y ^ (y >>> 11); + y = y ^ ((y << 7) & 0x9d2c5680); + y = y ^ ((y << 15) & 0xefc60000); + y = y ^ (y >>> 18); + + this.index = (this.index + 1) % 624; + + return y >>> 0; + } + + public nextIntRange(start: number, endExclusive: number) { + const range = endExclusive - start; + return Math.floor(this._nextInt() / (0x100000000 / range)) + start; + } + + private generateNumbers() { + for (let i = 0; i < 624; i++) { + const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff); + this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1); + if ((y % 2) !== 0) { + this.mt[i] = this.mt[i] ^ 0x9908b0df; + } + } + } +} diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index 1e9c342ebcd..f02e8a9bd50 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -5,32 +5,29 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MersenneTwister, getRandomEditInfos, toEdit, } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; -import { TextEdit } from 'vs/editor/common/core/textEdit'; -import { TextModelText } from 'vs/editor/common/model/textModelText'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { StringText } from 'vs/editor/common/core/textEdit'; +import { Random } from 'vs/editor/test/common/core/random'; suite('TextEdit', () => { suite('inverse', () => { ensureNoDisposablesAreLeakedInTestSuite(); function runTest(seed: number): void { - const rng = new MersenneTwister(seed); - const randomText = generateRandomMultilineString(rng, 10); - const model = createTextModel(randomText); + const rand = Random.create(seed); + const source = new StringText(rand.nextMultiLineString(10, new OffsetRange(0, 10))); - const edits = new TextEdit(getRandomEditInfos(model, rng.nextIntRange(1, 4), rng, true).map(e => toEdit(e))); - const invEdits = edits.inverse(new TextModelText(model)); + const edit = rand.nextTextEdit(source, rand.nextIntRange(1, 5)); + const invEdit = edit.inverse(source); - model.applyEdits(edits.edits); - model.applyEdits(invEdits.edits); + const s1 = edit.apply(source); + const s2 = invEdit.applyToString(s1); - assert.deepStrictEqual(model.getValue(), randomText); - model.dispose(); + assert.deepStrictEqual(s2, source.value); } test.skip('brute-force', () => { - for (let i = 0; i < 10_000; i++) { + for (let i = 0; i < 100_000; i++) { runTest(i); } }); @@ -40,23 +37,3 @@ suite('TextEdit', () => { } }); }); - -function generateRandomMultilineString(rng: MersenneTwister, numberOfLines: number, maximumLengthOfLines: number = 20): string { - let randomText: string = ''; - for (let i = 0; i < numberOfLines; i++) { - const lengthOfLine = rng.nextIntRange(0, maximumLengthOfLines + 1); - randomText += generateRandomSimpleString(rng, lengthOfLine) + '\n'; - } - return randomText; -} - -function generateRandomSimpleString(rng: MersenneTwister, stringLength: number): string { - const possibleCharacters: string = ' abcdefghijklmnopqrstuvwxyz0123456789'; - let randomText: string = ''; - for (let i = 0; i < stringLength; i++) { - const characterIndex = rng.nextIntRange(0, possibleCharacters.length); - randomText += possibleCharacters.charAt(characterIndex); - - } - return randomText; -} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 05f2063baf1..9155b32a9ca 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -11,6 +11,7 @@ import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/b import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; import { lengthAdd, lengthToObj, lengthToPosition, positionToLength, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { Random } from 'vs/editor/test/common/core/random'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; suite('combineTextEditInfos', () => { @@ -24,7 +25,7 @@ suite('combineTextEditInfos', () => { }); function runTest(seed: number) { - const rng = new MersenneTwister(seed); + const rng = Random.create(seed); const str = 'abcde\nfghij\nklmno\npqrst\n'; const textModelS0 = createTextModel(str); @@ -57,7 +58,7 @@ function runTest(seed: number) { textModelS2.dispose(); } -export function getRandomEditInfos(textModel: TextModel, count: number, rng: MersenneTwister, disjoint: boolean = false): TextEditInfo[] { +export function getRandomEditInfos(textModel: TextModel, count: number, rng: Random, disjoint: boolean = false): TextEditInfo[] { const edits: TextEditInfo[] = []; let i = 0; for (let j = 0; j < count; j++) { @@ -67,7 +68,7 @@ export function getRandomEditInfos(textModel: TextModel, count: number, rng: Mer return edits; } -function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: MersenneTwister): TextEditInfo { +function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Random): TextEditInfo { const textModelLength = textModel.getValueLength(); const offsetStart = rng.nextIntRange(rangeOffsetStart, textModelLength); const offsetEnd = rng.nextIntRange(offsetStart, textModelLength); @@ -78,7 +79,7 @@ function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Mers return new TextEditInfo(positionToLength(textModel.getPositionAt(offsetStart)), positionToLength(textModel.getPositionAt(offsetEnd)), toLength(lineCount, columnCount)); } -export function toEdit(editInfo: TextEditInfo): SingleTextEdit { +function toEdit(editInfo: TextEditInfo): SingleTextEdit { const l = lengthToObj(editInfo.newLength); let text = ''; @@ -89,56 +90,11 @@ export function toEdit(editInfo: TextEditInfo): SingleTextEdit { text += 'C'; } - return { - range: Range.fromPositions( + return new SingleTextEdit( + Range.fromPositions( lengthToPosition(editInfo.startOffset), lengthToPosition(editInfo.endOffset) ), text - }; -} - -// Generated by copilot -export class MersenneTwister { - private readonly mt = new Array(624); - private index = 0; - - constructor(seed: number) { - this.mt[0] = seed >>> 0; - for (let i = 1; i < 624; i++) { - const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); - this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0; - } - } - - public nextInt() { - if (this.index === 0) { - this.generateNumbers(); - } - - let y = this.mt[this.index]; - y = y ^ (y >>> 11); - y = y ^ ((y << 7) & 0x9d2c5680); - y = y ^ ((y << 15) & 0xefc60000); - y = y ^ (y >>> 18); - - this.index = (this.index + 1) % 624; - - return y >>> 0; - } - - public nextIntRange(start: number, endExclusive: number) { - const range = endExclusive - start; - return Math.floor(this.nextInt() / (0x100000000 / range)) + start; - } - - private generateNumbers() { - for (let i = 0; i < 624; i++) { - const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff); - this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1); - if ((y % 2) !== 0) { - this.mt[i] = this.mt[i] ^ 0x9908b0df; - } - } - } + ); } From fb865521ceb96845e578c3f296663eacc1425921 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 8 Mar 2024 15:15:35 +0100 Subject: [PATCH 075/141] Renames RangeLength to TextLength --- .../editor/browser/widget/diffEditor/utils.ts | 8 ++-- src/vs/editor/common/core/positionToOffset.ts | 10 ++--- src/vs/editor/common/core/rangeMapping.ts | 4 +- src/vs/editor/common/core/textEdit.ts | 14 +++---- .../core/{rangeLength.ts => textLength.ts} | 38 +++++++++---------- .../beforeEditPositionMapper.ts | 10 ++--- .../bracketPairsTree/length.ts | 16 ++++---- src/vs/editor/common/model/textModelText.ts | 6 +-- .../browser/inlineCompletionsModel.ts | 4 +- .../browser/singleTextEdit.ts | 4 +- .../mergeEditor/browser/model/rangeUtils.ts | 16 ++++---- .../mergeEditor/browser/view/lineAlignment.ts | 6 +-- .../mergeEditor/test/browser/mapping.test.ts | 12 +++--- 13 files changed, 74 insertions(+), 74 deletions(-) rename src/vs/editor/common/core/{rangeLength.ts => textLength.ts} (70%) diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index ebe47afd423..a1e263948f2 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -425,11 +425,11 @@ export function translatePosition(posInOriginal: Position, mappings: DetailedLin } } -function lengthBetweenPositions(position1: Position, position2: Position): RangeLength { +function lengthBetweenPositions(position1: Position, position2: Position): TextLength { if (position1.lineNumber === position2.lineNumber) { - return new RangeLength(0, position2.column - position1.column); + return new TextLength(0, position2.column - position1.column); } else { - return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts index e181b90173c..484c0a3265f 100644 --- a/src/vs/editor/common/core/positionToOffset.ts +++ b/src/vs/editor/common/core/positionToOffset.ts @@ -7,7 +7,7 @@ import { findLastIdxMonotonous } from 'vs/base/common/arraysFind'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; export class PositionOffsetTransformer { private readonly lineStartOffsetByLineIdx: number[]; @@ -47,12 +47,12 @@ export class PositionOffsetTransformer { ); } - getTextLength(offsetRange: OffsetRange): RangeLength { - return RangeLength.ofRange(this.getRange(offsetRange)); + getTextLength(offsetRange: OffsetRange): TextLength { + return TextLength.ofRange(this.getRange(offsetRange)); } - get textLength(): RangeLength { + get textLength(): TextLength { const lineIdx = this.lineStartOffsetByLineIdx.length - 1; - return new RangeLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]); + return new TextLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]); } } diff --git a/src/vs/editor/common/core/rangeMapping.ts b/src/vs/editor/common/core/rangeMapping.ts index 6e87efee7f7..379e046357d 100644 --- a/src/vs/editor/common/core/rangeMapping.ts +++ b/src/vs/editor/common/core/rangeMapping.ts @@ -6,7 +6,7 @@ import { findLastMonotonous } from 'vs/base/common/arraysFind'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; /** * Represents a list of mappings of ranges from one document to another. @@ -23,7 +23,7 @@ export class RangeMapping { if (mapping.original.containsPosition(position)) { return PositionOrRange.range(mapping.modified); } - const l = RangeLength.betweenPositions(mapping.original.getEndPosition(), position); + const l = TextLength.betweenPositions(mapping.original.getEndPosition(), position); return PositionOrRange.position(l.addToPosition(mapping.modified.getEndPosition())); } diff --git a/src/vs/editor/common/core/textEdit.ts b/src/vs/editor/common/core/textEdit.ts index ade6f7bf16a..e353361d953 100644 --- a/src/vs/editor/common/core/textEdit.ts +++ b/src/vs/editor/common/core/textEdit.ts @@ -8,7 +8,7 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { Position } from 'vs/editor/common/core/position'; import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; export class TextEdit { constructor(public readonly edits: readonly SingleTextEdit[]) { @@ -44,7 +44,7 @@ export class TextEdit { break; } - const len = RangeLength.ofText(edit.text); + const len = TextLength.ofText(edit.text); if (position.isBefore(end)) { const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); const endPos = len.addToPosition(startPos); @@ -132,7 +132,7 @@ export class TextEdit { let lineOffset = 0; let columnOffset = 0; for (const edit of this.edits) { - const textLength = RangeLength.ofText(edit.text); + const textLength = TextLength.ofText(edit.text); const newRangeStart = Position.lift({ lineNumber: edit.range.startLineNumber + lineOffset, column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) @@ -172,7 +172,7 @@ function rangeFromPositions(start: Position, end: Position): Range { export abstract class AbstractText { abstract getValueOfRange(range: Range): string; - abstract readonly length: RangeLength; + abstract readonly length: TextLength; get endPositionExclusive(): Position { return this.length.addToPosition(new Position(1, 1)); @@ -205,9 +205,9 @@ export class LineBasedText extends AbstractText { return result; } - get length(): RangeLength { + get length(): TextLength { const lastLine = this._getLineContent(this._lineCount); - return new RangeLength(this._lineCount - 1, lastLine.length); + return new TextLength(this._lineCount - 1, lastLine.length); } } @@ -222,7 +222,7 @@ export class StringText extends AbstractText { return this._t.getOffsetRange(range).substring(this.value); } - get length(): RangeLength { + get length(): TextLength { return this._t.textLength; } } diff --git a/src/vs/editor/common/core/rangeLength.ts b/src/vs/editor/common/core/textLength.ts similarity index 70% rename from src/vs/editor/common/core/rangeLength.ts rename to src/vs/editor/common/core/textLength.ts index d01fca41c1f..632895c55fd 100644 --- a/src/vs/editor/common/core/rangeLength.ts +++ b/src/vs/editor/common/core/textLength.ts @@ -8,33 +8,33 @@ import { Range } from 'vs/editor/common/core/range'; /** * Represents a non-negative length of text in terms of line and column count. */ -export class RangeLength { - public static zero = new RangeLength(0, 0); +export class TextLength { + public static zero = new TextLength(0, 0); - public static lengthDiffNonNegative(start: RangeLength, end: RangeLength): RangeLength { + public static lengthDiffNonNegative(start: TextLength, end: TextLength): TextLength { if (end.isLessThan(start)) { - return RangeLength.zero; + return TextLength.zero; } if (start.lineCount === end.lineCount) { - return new RangeLength(0, end.columnCount - start.columnCount); + return new TextLength(0, end.columnCount - start.columnCount); } else { - return new RangeLength(end.lineCount - start.lineCount, end.columnCount); + return new TextLength(end.lineCount - start.lineCount, end.columnCount); } } - public static betweenPositions(position1: Position, position2: Position): RangeLength { + public static betweenPositions(position1: Position, position2: Position): TextLength { if (position1.lineNumber === position2.lineNumber) { - return new RangeLength(0, position2.column - position1.column); + return new TextLength(0, position2.column - position1.column); } else { - return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } public static ofRange(range: Range) { - return RangeLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); + return TextLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); } - public static ofText(text: string): RangeLength { + public static ofText(text: string): TextLength { let line = 0; let column = 0; for (const c of text) { @@ -45,7 +45,7 @@ export class RangeLength { column++; } } - return new RangeLength(line, column); + return new TextLength(line, column); } constructor( @@ -57,36 +57,36 @@ export class RangeLength { return this.lineCount === 0 && this.columnCount === 0; } - public isLessThan(other: RangeLength): boolean { + public isLessThan(other: TextLength): boolean { if (this.lineCount !== other.lineCount) { return this.lineCount < other.lineCount; } return this.columnCount < other.columnCount; } - public isGreaterThan(other: RangeLength): boolean { + public isGreaterThan(other: TextLength): boolean { if (this.lineCount !== other.lineCount) { return this.lineCount > other.lineCount; } return this.columnCount > other.columnCount; } - public equals(other: RangeLength): boolean { + public equals(other: TextLength): boolean { return this.lineCount === other.lineCount && this.columnCount === other.columnCount; } - public compare(other: RangeLength): number { + public compare(other: TextLength): number { if (this.lineCount !== other.lineCount) { return this.lineCount - other.lineCount; } return this.columnCount - other.columnCount; } - public add(other: RangeLength): RangeLength { + public add(other: TextLength): TextLength { if (other.lineCount === 0) { - return new RangeLength(this.lineCount, this.columnCount + other.columnCount); + return new TextLength(this.lineCount, this.columnCount + other.columnCount); } else { - return new RangeLength(this.lineCount + other.lineCount, other.columnCount); + return new TextLength(this.lineCount + other.lineCount, other.columnCount); } } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index e596c00e983..1f95f84df48 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, lengthOfString, lengthToObj, positionToLength, toLength } from './length'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { IModelContentChange } from 'vs/editor/common/textModelEvents'; export class TextEditInfo { @@ -74,7 +74,7 @@ export class BeforeEditPositionMapper { return lengthDiffNonNegative(offset, nextChangeOffset); } - private translateOldToCur(oldOffsetObj: RangeLength): Length { + private translateOldToCur(oldOffsetObj: TextLength): Length { if (oldOffsetObj.lineCount === this.deltaLineIdxInOld) { return toLength(oldOffsetObj.lineCount + this.deltaOldToNewLineCount, oldOffsetObj.columnCount + this.deltaOldToNewColumnCount); } else { @@ -127,9 +127,9 @@ class TextEditInfoCache { return new TextEditInfoCache(edit.startOffset, edit.endOffset, edit.newLength); } - public readonly endOffsetBeforeObj: RangeLength; - public readonly endOffsetAfterObj: RangeLength; - public readonly offsetObj: RangeLength; + public readonly endOffsetBeforeObj: TextLength; + public readonly endOffsetAfterObj: TextLength; + public readonly offsetObj: TextLength; constructor( startOffset: Length, diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index 0ff10813857..d41a62233e5 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts @@ -6,7 +6,7 @@ import { splitLines } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; /** * The end must be greater than or equal to the start. @@ -49,11 +49,11 @@ export function toLength(lineCount: number, columnCount: number): Length { return (lineCount * factor + columnCount) as any as Length; } -export function lengthToObj(length: Length): RangeLength { +export function lengthToObj(length: Length): TextLength { const l = length as any as number; const lineCount = Math.floor(l / factor); const columnCount = l - lineCount * factor; - return new RangeLength(lineCount, columnCount); + return new TextLength(lineCount, columnCount); } export function lengthGetLineCount(length: Length): number { @@ -148,11 +148,11 @@ export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range { return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1); } -export function lengthOfRange(range: Range): RangeLength { +export function lengthOfRange(range: Range): TextLength { if (range.startLineNumber === range.endLineNumber) { - return new RangeLength(0, range.endColumn - range.startColumn); + return new TextLength(0, range.endColumn - range.startColumn); } else { - return new RangeLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new TextLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } @@ -167,9 +167,9 @@ export function lengthOfString(str: string): Length { return toLength(lines.length - 1, lines[lines.length - 1].length); } -export function lengthOfStringObj(str: string): RangeLength { +export function lengthOfStringObj(str: string): TextLength { const lines = splitLines(str); - return new RangeLength(lines.length - 1, lines[lines.length - 1].length); + return new TextLength(lines.length - 1, lines[lines.length - 1].length); } /** diff --git a/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts index 36f73a63d67..0a603fa1ed2 100644 --- a/src/vs/editor/common/model/textModelText.ts +++ b/src/vs/editor/common/model/textModelText.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { AbstractText } from 'vs/editor/common/core/textEdit'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { ITextModel } from 'vs/editor/common/model'; export class TextModelText extends AbstractText { @@ -17,9 +17,9 @@ export class TextModelText extends AbstractText { return this._textModel.getValueInRange(range); } - get length(): RangeLength { + get length(): TextLength { const lastLineNumber = this._textModel.getLineCount(); const lastLineLen = this._textModel.getLineLength(lastLineNumber); - return new RangeLength(lastLineNumber - 1, lastLineLen); + return new TextLength(lastLineNumber - 1, lastLineLen); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 57357fd47e1..996471bde48 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -28,7 +28,7 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetCon import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { singleTextEditAugments, computeGhostText, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; export enum VersionIdChangeReason { Undo, @@ -444,7 +444,7 @@ export class InlineCompletionsModel extends Disposable { } if (completion.source.provider.handlePartialAccept) { - const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), RangeLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); + const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); // This assumes that the inline completion and the model use the same EOL style. const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); completion.source.provider.handlePartialAccept( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts index d38d1e41f57..750eb459829 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts @@ -7,7 +7,7 @@ import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff'; import { commonPrefixLength, getLeadingWhitespace } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; @@ -19,7 +19,7 @@ export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextM } const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); - const start = RangeLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); + const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); const text = edit.text.substring(commonPrefixLen); const range = Range.fromPositions(start, edit.range.getEndPosition()); return new SingleTextEdit(range, text); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts index fef5aaad5d7..dd224530145 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts @@ -5,7 +5,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; export function rangeContainsPosition(range: Range, position: Position): boolean { if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { @@ -20,23 +20,23 @@ export function rangeContainsPosition(range: Range, position: Position): boolean return true; } -export function lengthOfRange(range: Range): RangeLength { +export function lengthOfRange(range: Range): TextLength { if (range.startLineNumber === range.endLineNumber) { - return new RangeLength(0, range.endColumn - range.startColumn); + return new TextLength(0, range.endColumn - range.startColumn); } else { - return new RangeLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new TextLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } -export function lengthBetweenPositions(position1: Position, position2: Position): RangeLength { +export function lengthBetweenPositions(position1: Position, position2: Position): TextLength { if (position1.lineNumber === position2.lineNumber) { - return new RangeLength(0, position2.column - position1.column); + return new TextLength(0, position2.column - position1.column); } else { - return new RangeLength(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } -export function addLength(position: Position, length: RangeLength): Position { +export function addLength(position: Position, length: TextLength): Position { if (length.lineCount === 0) { return new Position(position.lineNumber, position.column + length.columnCount); } else { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts index 6d4f28beb1e..c6d9664b4be 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts @@ -8,7 +8,7 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { isDefined } from 'vs/base/common/types'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { addLength, lengthBetweenPositions, lengthOfRange } from 'vs/workbench/contrib/mergeEditor/browser/model/rangeUtils'; @@ -49,7 +49,7 @@ export function getAlignments(m: ModifiedBaseRange): LineAlignment[] { if (shouldAdd) { result.push(lineAlignment); } else { - if (m.length.isGreaterThan(new RangeLength(1, 0))) { + if (m.length.isGreaterThan(new TextLength(1, 0))) { result.push([ m.output1Pos ? m.output1Pos.lineNumber + 1 : undefined, m.inputPos.lineNumber + 1, @@ -75,7 +75,7 @@ interface CommonRangeMapping { output1Pos: Position | undefined; output2Pos: Position | undefined; inputPos: Position; - length: RangeLength; + length: TextLength; } function toEqualRangeMappings(diffs: RangeMapping[], inputRange: Range, outputRange: Range): RangeMapping[] { diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index 9d824d4c7fa..85c475e3c2f 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { RangeLength } from 'vs/editor/common/core/rangeLength'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { DocumentRangeMap, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; suite('merge editor mapping', () => { @@ -53,19 +53,19 @@ function parsePos(str: string): Position { return new Position(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function parseLengthObj(str: string): RangeLength { +function parseLengthObj(str: string): TextLength { const [lineCount, columnCount] = str.split(':'); - return new RangeLength(parseInt(lineCount, 10), parseInt(columnCount, 10)); + return new TextLength(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function toPosition(length: RangeLength): Position { +function toPosition(length: TextLength): Position { return new Position(length.lineCount + 1, length.columnCount + 1); } function createDocumentRangeMap(items: ([string, string] | string)[]) { const mappings: RangeMapping[] = []; - let lastLen1 = new RangeLength(0, 0); - let lastLen2 = new RangeLength(0, 0); + let lastLen1 = new TextLength(0, 0); + let lastLen2 = new TextLength(0, 0); for (const item of items) { if (typeof item === 'string') { const len = parseLengthObj(item); From 16064ae9ebee0722d9f9ad65e3496dc6b5d85837 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 8 Mar 2024 12:30:52 -0300 Subject: [PATCH 076/141] Allow empty 'values' array for variables (#207168) Allow an empty 'values' array for variables --- src/vs/workbench/contrib/chat/browser/chatVariables.ts | 4 +--- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 147b267245c..3de4fb0c209 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -40,9 +40,7 @@ export class ChatVariablesService implements IChatVariablesService { const data = this._resolver.get(part.variableName.toLowerCase()); if (data) { jobs.push(data.resolver(prompt.text, part.variableArg, model, progress, token).then(values => { - if (values?.length) { - resolvedVariables[i] = { name: part.variableName, range: part.range, values }; - } + resolvedVariables[i] = { name: part.variableName, range: part.range, values: values ?? [] }; }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index f3b59ac9e84..0f872032de2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -266,6 +266,9 @@ declare module 'vscode' { readonly range: [start: number, end: number]; // TODO@API decouple of resolve API, use `value: string | Uri | (maybe) unknown?` + /** + * The values of the variable. Can be an empty array if the variable doesn't currently have a value. + */ readonly values: ChatVariableValue[]; } From a9ab31da9cfa2d45bac4bda59a980cc701d497e5 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 8 Mar 2024 13:59:12 +0100 Subject: [PATCH 077/141] Refactors: Reduces assumptions about line height. This contains the parts of https://github.com/microsoft/vscode/pull/194609 that we can already merge. While it does not implement dynamic line heights, it is a first step in that direction. Co-authored-by: Remco Haszing --- src/vs/editor/browser/view/viewLayer.ts | 10 +++--- src/vs/editor/browser/view/viewOverlays.ts | 30 +++++----------- .../currentLineHighlight.css | 6 +++- .../currentLineHighlight.ts | 7 ++-- .../viewParts/decorations/decorations.css | 3 +- .../viewParts/decorations/decorations.ts | 23 +++++-------- .../viewParts/indentGuides/indentGuides.css | 1 + .../viewParts/indentGuides/indentGuides.ts | 6 +--- .../viewParts/lineNumbers/lineNumbers.css | 2 +- .../browser/viewParts/lines/viewLine.ts | 8 ++--- .../browser/viewParts/lines/viewLines.css | 5 +++ .../viewParts/selections/selections.ts | 34 +++++++------------ .../viewParts/whitespace/whitespace.ts | 2 +- .../browser/widget/codeEditor/editor.css | 9 +++++ .../editor/common/viewLayout/linesLayout.ts | 3 +- .../viewLayout/viewLinesViewportData.ts | 3 ++ src/vs/editor/common/viewModel.ts | 5 +++ 17 files changed, 75 insertions(+), 82 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index c15239ec8b1..bbbb0dd9d73 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -22,12 +22,12 @@ export interface IVisibleLine extends ILine { * Return null if the HTML should not be touched. * Return the new HTML otherwise. */ - renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean; + renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean; /** * Layout the line. */ - layoutLine(lineNumber: number, deltaTop: number): void; + layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void; } export interface ILine { @@ -465,7 +465,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN]); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this.viewportData.lineHeight); } } @@ -573,7 +573,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -603,7 +603,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 83a3cc05d6f..1041fd58a58 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -9,7 +9,6 @@ import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { IVisibleLine, IVisibleLinesHost, VisibleLinesCollection } from 'vs/editor/browser/view/viewLayer'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -71,7 +70,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost | null; private _renderedContent: string | null; - private _lineHeight: number; - constructor(configuration: IEditorConfiguration, dynamicOverlays: DynamicViewOverlay[]) { - this._configuration = configuration; - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); + constructor(dynamicOverlays: DynamicViewOverlay[]) { this._dynamicOverlays = dynamicOverlays; this._domNode = null; @@ -180,11 +169,8 @@ export class ViewOverlayLine implements IVisibleLine { public onTokensChanged(): void { // Nothing } - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); - } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { let result = ''; for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) { const dynamicOverlay = this._dynamicOverlays[i]; @@ -198,10 +184,10 @@ export class ViewOverlayLine implements IVisibleLine { this._renderedContent = result; - sb.appendString('
'); sb.appendString(result); sb.appendString('
'); @@ -209,10 +195,10 @@ export class ViewOverlayLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._domNode) { this._domNode.setTop(deltaTop); - this._domNode.setHeight(this._lineHeight); + this._domNode.setHeight(lineHeight); } } } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css index 2a0e39dffa7..403e255fac8 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css @@ -9,6 +9,7 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } .monaco-editor .margin-view-overlays .current-line { @@ -17,8 +18,11 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } -.monaco-editor .margin-view-overlays .current-line.current-line-margin.current-line-margin-both { +.monaco-editor + .margin-view-overlays + .current-line.current-line-margin.current-line-margin-both { border-right: 0; } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 64649e0b835..b35970ee373 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -18,7 +18,6 @@ import { Position } from 'vs/editor/common/core/position'; export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - protected _lineHeight: number; protected _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; protected _wordWrap: boolean; protected _contentLeft: number; @@ -39,7 +38,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -89,7 +87,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -208,7 +205,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-both' : '') + (exact ? ' current-line-exact' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return this._shouldRenderInContent(); @@ -221,7 +218,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return true; diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.css b/src/vs/editor/browser/viewParts/decorations/decorations.css index 37c39f620e8..4c755e2dbf8 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/src/vs/editor/browser/viewParts/decorations/decorations.css @@ -9,4 +9,5 @@ */ .monaco-editor .lines-content .cdr { position: absolute; -} \ No newline at end of file + height: 100%; +} diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index fe495466b1d..a3baa510464 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -15,7 +15,6 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; export class DecorationsOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _renderResult: string[] | null; @@ -23,7 +22,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._renderResult = null; @@ -40,7 +38,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; } @@ -116,7 +113,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; @@ -130,9 +126,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { const decorationOutput = ( '
' + + '" style="left:0;width:100%;">' ); const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); @@ -145,7 +139,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; let prevClassName: string | null = null; @@ -176,7 +169,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { // flush previous decoration if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } prevClassName = className; @@ -186,11 +179,11 @@ export class DecorationsOverlay extends DynamicViewOverlay { } if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } } - private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, lineHeight: string, visibleStartLineNumber: number, output: string[]): void { + private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, visibleStartLineNumber: number, output: string[]): void { const linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); if (!linesVisibleRanges) { return; @@ -222,12 +215,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { + className + '" style="left:' + String(visibleRange.left) + + 'px;width:' + (expandToLeft ? - 'px;width:100%;height:' : - ('px;width:' + String(visibleRange.width) + 'px;height:') + '100%;' : + (String(visibleRange.width) + 'px;') ) - + lineHeight - + 'px;">' + + '">' ); output[lineIndex] += decorationOutput; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index ed132669757..6aacf7c2126 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -6,4 +6,5 @@ .monaco-editor .lines-content .core-guide { position: absolute; box-sizing: border-box; + height: 100%; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index a93cf75a530..50b0b2b8661 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -22,7 +22,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; private _primaryPosition: Position | null; - private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[] | null; private _maxIndentLeft: number; @@ -37,7 +36,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -60,7 +58,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -114,7 +111,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const scrollWidth = ctx.scrollWidth; - const lineHeight = this._lineHeight; const activeCursorPosition = this._primaryPosition; @@ -150,7 +146,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { )?.left ?? (left + this._spaceWidth)) - left : this._spaceWidth; - result += `
`; + result += `
`; } output[lineIndex] = result; } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 774ffef273d..2961137b032 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .margin-view-overlays .line-numbers { + bottom: 0; font-variant-numeric: tabular-nums; position: absolute; text-align: right; @@ -11,7 +12,6 @@ vertical-align: middle; box-sizing: border-box; cursor: default; - height: 100%; } .monaco-editor .relative-current-line-number { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index e4174a2f286..9a5d2f556bf 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -151,7 +151,7 @@ export class ViewLine implements IVisibleLine { return false; } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; @@ -222,7 +222,7 @@ export class ViewLine implements IVisibleLine { sb.appendString('
'); @@ -255,10 +255,10 @@ export class ViewLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); - this._renderedViewLine.domNode.setHeight(this._options.lineHeight); + this._renderedViewLine.domNode.setHeight(lineHeight); } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index fe686d3e441..5bb02a5338c 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,6 +63,11 @@ width: 100%; } +.monaco-editor .view-line > span { + bottom: 0; + position: absolute; +} + .monaco-editor .mtkw { color: var(--vscode-editorWhitespace-foreground) !important; } diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index efceef0e5c3..d53a5126e62 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -68,7 +68,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { private static readonly ROUNDED_PIECE_WIDTH = 10; private readonly _context: ViewContext; - private _lineHeight: number; private _roundedSelection: boolean; private _typicalHalfwidthCharacterWidth: number; private _selections: Range[]; @@ -78,7 +77,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._selections = []; @@ -96,7 +94,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; @@ -255,19 +252,16 @@ export class SelectionsOverlay extends DynamicViewOverlay { return linesVisibleRanges; } - private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string { + private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string { return ( '
' + + '" style="' + + 'top:' + top.toString() + 'px;' + + 'bottom:' + bottom.toString() + 'px;' + + 'left:' + left.toString() + 'px;' + + 'width:' + width.toString() + 'px;' + + '">
' ); } @@ -277,8 +271,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { } const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle; - const fullLineHeight = (this._lineHeight).toString(); - const reducedLineHeight = (this._lineHeight - 1).toString(); const firstLineNumber = visibleRanges[0].lineNumber; const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber; @@ -288,8 +280,8 @@ export class SelectionsOverlay extends DynamicViewOverlay { const lineNumber = lineVisibleRanges.lineNumber; const lineIndex = lineNumber - visibleStartLineNumber; - const lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight; const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0; + const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0; let innerCornerOutput = ''; let restOfSelectionOutput = ''; @@ -304,7 +296,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { // Reverse rounded corner to the left // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -314,13 +306,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (startStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) { // Reverse rounded corner to the right // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -330,7 +322,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (endStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } } @@ -351,7 +343,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } } - restOfSelectionOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width); + restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width); } output2[lineIndex][0] += innerCornerOutput; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 489293a01b8..3bd29fc5e1e 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -235,7 +235,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { if (USE_SVG) { maxLeft = Math.round(maxLeft + spaceWidth); return ( - `` + `` + result + `` ); diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 1d60940158a..09c4a32f141 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -56,6 +56,15 @@ top: 0; } +.monaco-editor .view-overlays > div, .monaco-editor .margin-view-overlays > div { + position: absolute; + width: 100%; +} + +.monaco-editor .view-overlays > div > div, .monaco-editor .margin-view-overlays > div > div { + bottom: 0; +} + /* .monaco-editor .auto-closed-character { opacity: 0.3; diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 7bb55aeef6e..71bf9d5b956 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -727,7 +727,8 @@ export class LinesLayout { relativeVerticalOffset: linesOffsets, centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, - completelyVisibleEndLineNumber: completelyVisibleEndLineNumber + completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, + lineHeight: this._lineHeight, }; } diff --git a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 8ddcfddb99d..6e072c52648 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -46,6 +46,8 @@ export class ViewportData { private readonly _model: IViewModel; + public readonly lineHeight: number; + constructor( selections: Selection[], partialData: IPartialViewLinesViewportData, @@ -57,6 +59,7 @@ export class ViewportData { this.endLineNumber = partialData.endLineNumber | 0; this.relativeVerticalOffset = partialData.relativeVerticalOffset; this.bigNumbersDelta = partialData.bigNumbersDelta | 0; + this.lineHeight = partialData.lineHeight | 0; this.whitespaceViewportData = whitespaceViewportData; this._model = model; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 4f92417e89b..29a01bcf904 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -181,6 +181,11 @@ export interface IPartialViewLinesViewportData { * The last completely visible line number. */ readonly completelyVisibleEndLineNumber: number; + + /** + * The height of a line. + */ + readonly lineHeight: number; } export interface IViewWhitespaceViewportData { From 967170aa0a0f746338dbd080b4a0a04a7a67a7fb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:41:40 +0100 Subject: [PATCH 078/141] Git - add the capability to filter git log based on the author (#207169) --- extensions/git/src/api/git.d.ts | 1 + extensions/git/src/git.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 6a53d893352..cf462435a2c 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -143,6 +143,7 @@ export interface LogOptions { readonly reverse?: boolean; readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; + readonly author?: string; } export interface CommitOptions { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 738295bdec1..710d7a4d110 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1161,6 +1161,10 @@ export class Repository { args.push(`-n${options?.maxEntries ?? 32}`); } + if (options?.author) { + args.push(`--author="${options.author}"`); + } + if (options?.path) { args.push('--', options.path); } From cb79a8f9228fd08081f8050522b864c31f683fe5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 8 Mar 2024 16:58:47 +0100 Subject: [PATCH 079/141] watcher - allow non-existent paths when correlating (#207145) --- src/vs/platform/files/common/files.ts | 8 - src/vs/platform/files/common/watcher.ts | 2 +- .../files/node/watcher/baseWatcher.ts | 173 ++++++++++++++++++ .../files/node/watcher/nodejs/nodejsClient.ts | 2 +- .../node/watcher/nodejs/nodejsWatcher.ts | 33 ++-- .../node/watcher/parcel/parcelWatcher.ts | 37 ++-- .../node/nodejsWatcher.integrationTest.ts | 76 +++++++- .../node/parcelWatcher.integrationTest.ts | 58 +++++- 8 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 src/vs/platform/files/node/watcher/baseWatcher.ts diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0bc285f082e..e8bcce418a8 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -243,14 +243,6 @@ export interface IFileService { */ createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher; - /** - * Allows to start a watcher that reports file/folder change events on the provided resource. - * - * The watcher runs correlated and thus, file events will be reported on the returned - * `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event. - */ - watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher; - /** * Allows to start a watcher that reports file/folder change events on the provided resource. * diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index ae97f833078..95b3b3363f6 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -71,7 +71,7 @@ export function isRecursiveWatchRequest(request: IWatchRequest): request is IRec export type IUniversalWatchRequest = IRecursiveWatchRequest | INonRecursiveWatchRequest; -interface IWatcher { +export interface IWatcher { /** * A normalized file change event from the raw events diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts new file mode 100644 index 00000000000..845d7bbe7c8 --- /dev/null +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { watchFile, unwatchFile, Stats } from 'fs'; +import { Disposable, DisposableMap, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ILogMessage, IUniversalWatchRequest, IWatcher } from 'vs/platform/files/common/watcher'; +import { Emitter, Event } from 'vs/base/common/event'; +import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; + +export abstract class BaseWatcher extends Disposable implements IWatcher { + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + protected readonly _onDidLogMessage = this._register(new Emitter()); + readonly onDidLogMessage = this._onDidLogMessage.event; + + private mapWatchMissingRequestPathToCorrelationId = this._register(new DisposableMap()); + + private allWatchRequests = new Set(); + private suspendedWatchRequests = new Set(); + + protected readonly missingRequestPathPollingInterval: number | undefined; + + async watch(requests: IUniversalWatchRequest[]): Promise { + this.allWatchRequests = new Set([...requests]); + + const correlationIds = new Set(); + for (const request of requests) { + + // Request with correlation: watch request path to support + // watching paths that do not exist yet or are potentially + // being deleted and recreated. + // + // We are not doing this for all watch requests yet to see + // how it goes, thus its limitd to correlated requests. + + if (typeof request.correlationId === 'number') { + correlationIds.add(request.correlationId); + + if (!this.mapWatchMissingRequestPathToCorrelationId.has(request.correlationId)) { + this.mapWatchMissingRequestPathToCorrelationId.set(request.correlationId, this.watchMissingRequestPath(request)); + } + } + } + + // Remove all watched correlated paths that are no longer + // needed because the request is no longer there + for (const [correlationId] of this.mapWatchMissingRequestPathToCorrelationId) { + if (!correlationIds.has(correlationId)) { + this.mapWatchMissingRequestPathToCorrelationId.deleteAndDispose(correlationId); + } + } + + // Remove all suspended requests that are no longer needed + for (const request of this.suspendedWatchRequests) { + if (!this.allWatchRequests.has(request)) { + this.suspendedWatchRequests.delete(request); + } + } + + return this.updateWatchers(); + } + + private updateWatchers(): Promise { + return this.doWatch(Array.from(this.allWatchRequests).filter(request => !this.suspendedWatchRequests.has(request))); + } + + private watchMissingRequestPath(request: IUniversalWatchRequest): IDisposable { + if (typeof request.correlationId !== 'number') { + return Disposable.None; // for now limit this to correlated watch requests only (reduces surface) + } + + const that = this; + const resource = URI.file(request.path); + + let disposed = false; + let pathNotFound = false; + + const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => { + if (disposed) { + return; // return early if already disposed + } + + const currentPathNotFound = this.isPathNotFound(curr); + const previousPathNotFound = this.isPathNotFound(prev); + const oldPathNotFound = pathNotFound; + pathNotFound = currentPathNotFound; + + // Watch path created: resume watching request + if ( + (previousPathNotFound && !currentPathNotFound) || // file was created + (oldPathNotFound && !currentPathNotFound && !previousPathNotFound) // file was created from a rename + ) { + this.trace(`fs.watchFile() detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); + + // Emit as event + const event: IFileChange = { resource, type: FileChangeType.ADDED, cId: request.correlationId }; + that._onDidChangeFile.fire([event]); + this.traceEvent(event, request); + + this.suspendedWatchRequests.delete(request); + this.updateWatchers(); + } + + // Watch path deleted or never existed: suspend watching request + else if (currentPathNotFound) { + this.trace(`fs.watchFile() detected ${request.path} not found, suspending watcher (correlationId: ${request.correlationId})`); + + if (!previousPathNotFound) { + const event: IFileChange = { resource, type: FileChangeType.DELETED, cId: request.correlationId }; + that._onDidChangeFile.fire([event]); + this.traceEvent(event, request); + } + + this.suspendedWatchRequests.add(request); + this.updateWatchers(); + } + }; + + this.trace(`starting fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); + try { + watchFile(request.path, { persistent: false, interval: this.missingRequestPathPollingInterval }, watchFileCallback); + } catch (error) { + this.warn(`fs.watchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); + + return Disposable.None; + } + + return toDisposable(() => { + this.trace(`stopping fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); + + disposed = true; + + this.suspendedWatchRequests.delete(request); + + try { + unwatchFile(request.path, watchFileCallback); + } catch (error) { + this.warn(`fs.unwatchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); + } + }); + } + + private isPathNotFound(stats: Stats): boolean { + return stats.ctimeMs === 0 && stats.ino === 0; + } + + async stop(): Promise { + this.mapWatchMissingRequestPathToCorrelationId.clearAndDisposeAll(); + this.suspendedWatchRequests.clear(); + } + + protected shouldRestartWatching(request: IUniversalWatchRequest): boolean { + return typeof request.correlationId !== 'number'; + } + + protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void { + const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; + this.trace(typeof request.correlationId === 'number' ? `${traceMsg} (correlationId: ${request.correlationId})` : traceMsg); + } + + protected abstract doWatch(requests: IUniversalWatchRequest[]): Promise; + + protected abstract trace(message: string): void; + protected abstract warn(message: string): void; + + abstract onDidError: Event; + abstract setVerboseLogging(enabled: boolean): Promise; +} diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts index 11eb6b8a109..2a662eb7e05 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts @@ -21,6 +21,6 @@ export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient { } protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher { - return disposables.add(new NodeJSWatcher()); + return disposables.add(new NodeJSWatcher()) satisfies INonRecursiveWatcher; } } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index dac55a138c5..1cbbe49f025 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { patternsEquals } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { isLinux } from 'vs/base/common/platform'; -import { IFileChange } from 'vs/platform/files/common/files'; -import { ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; +import { INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; export interface INodeJSWatcherInstance { @@ -24,13 +23,7 @@ export interface INodeJSWatcherInstance { readonly request: INonRecursiveWatchRequest; } -export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { - - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter()); - readonly onDidLogMessage = this._onDidLogMessage.event; +export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { readonly onDidError = Event.None; @@ -38,7 +31,7 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { private verboseLogging = false; - async watch(requests: INonRecursiveWatchRequest[]): Promise { + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests const normalizedRequests = this.normalizeRequests(requests); @@ -90,7 +83,9 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { this.watchers.set(request.path, watcher); } - async stop(): Promise { + override async stop(): Promise { + await super.stop(); + for (const [path] of this.watchers) { this.stopWatching(path); } @@ -134,13 +129,19 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { } } - private trace(message: string): void { + protected trace(message: string): void { if (this.verboseLogging) { this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); } } - private toMessage(message: string, watcher?: INodeJSWatcherInstance): string { - return watcher ? `[File Watcher (node.js)] ${message} (path: ${watcher.request.path})` : `[File Watcher (node.js)] ${message}`; + protected warn(message: string): void { + if (this.verboseLogging) { + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message) }); + } + } + + private toMessage(message: string): string { + return `[File Watcher (node.js)] ${message}`; } } diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index c51b58db863..8a4b6404da2 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -13,7 +13,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { randomPath } from 'vs/base/common/extpath'; import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { normalizeNFC } from 'vs/base/common/normalization'; import { dirname, normalize } from 'vs/base/common/path'; @@ -21,7 +21,7 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; +import { coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; export interface IParcelWatcherInstance { @@ -58,7 +58,7 @@ export interface IParcelWatcherInstance { stop(): Promise; } -export class ParcelWatcher extends Disposable implements IRecursiveWatcher { +export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map( [ @@ -70,12 +70,6 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter()); - readonly onDidLogMessage = this._onDidLogMessage.event; - private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; @@ -120,7 +114,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { process.on('unhandledRejection', error => this.onUnexpectedError(error)); } - async watch(requests: IRecursiveWatchRequest[]): Promise { + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests const normalizedRequests = this.normalizeRequests(requests); @@ -370,8 +364,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Logging if (this.verboseLogging) { for (const event of events) { - const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; - this.trace(typeof watcher.request.correlationId === 'number' ? `${traceMsg} (correlationId: ${watcher.request.correlationId})` : traceMsg); + this.traceEvent(event, watcher.request); } } @@ -383,7 +376,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.warn(`started ignoring events due to too many file change events at once (incoming: ${events.length}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); } else { if (this.throttledFileChangesEmitter.pending > 0) { - this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); + this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`, watcher); } } } @@ -466,8 +459,14 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { private onWatchedPathDeleted(watcher: IParcelWatcherInstance): void { this.warn('Watcher shutdown because watched path got deleted', watcher); + if (!this.shouldRestartWatching(watcher.request)) { + return; // return if this deletion is handled outside + } + const parentPath = dirname(watcher.request.path); if (existsSync(parentPath)) { + this.trace('Trying to watch on the parent path to restart the watcher...', watcher); + const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => { if (watcher.token.isCancellationRequested) { return; // return early when disposed @@ -522,7 +521,9 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } - async stop(): Promise { + override async stop(): Promise { + await super.stop(); + for (const [path] of this.watchers) { await this.stopWatching(path); } @@ -559,7 +560,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { private async stopWatching(path: string): Promise { const watcher = this.watchers.get(path); if (watcher) { - this.trace(`stopping file watcher on ${watcher.request.path}`); + this.trace(`stopping file watcher`, watcher); this.watchers.delete(path); @@ -664,13 +665,13 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.verboseLogging = enabled; } - private trace(message: string) { + protected trace(message: string, watcher?: IParcelWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); } } - private warn(message: string, watcher?: IParcelWatcherInstance) { + protected warn(message: string, watcher?: IParcelWatcherInstance) { this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 74dbb343c97..28e9ded4c54 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -20,6 +20,7 @@ import { FileAccess } from 'vs/base/common/network'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { Emitter, Event } from 'vs/base/common/event'; // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in @@ -30,9 +31,16 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; class TestNodeJSWatcher extends NodeJSWatcher { - override async watch(requests: INonRecursiveWatchRequest[]): Promise { - await super.watch(requests); + protected override readonly missingRequestPathPollingInterval = 100; + + private readonly _onDidWatch = this._register(new Emitter()); + readonly onDidWatch = this._onDidWatch.event; + + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { + await super.doWatch(requests); await this.whenReady(); + + this._onDidWatch.fire(); } async whenReady(): Promise { @@ -432,7 +440,7 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; return basicCrudTest(join(link, 'newFile.txt')); }); - async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number): Promise { + async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number, awaitWatchAfterAdd?: boolean): Promise { let changeFuture: Promise; // New file @@ -440,6 +448,9 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, correlationId, expectedCount); await Promises.writeFile(filePath, 'Hello World'); await changeFuture; + if (awaitWatchAfterAdd) { + await Event.toPromise(watcher.onDidWatch); + } } // Change file @@ -559,4 +570,63 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; await basicCrudTest(join(testDir, 'newFile.txt'), undefined, null, 3); await basicCrudTest(join(testDir, 'otherNewFile.txt'), undefined, null, 3); }); + + test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () { + const filePath = join(testDir, 'not-found.txt'); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + + await basicCrudTest(filePath, undefined, 1, undefined, true); + await basicCrudTest(filePath, undefined, 1, undefined, true); + }); + + test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () { + const filePath = join(testDir, 'lorem.txt'); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + + await basicCrudTest(filePath, true, 1); + await basicCrudTest(filePath, undefined, 1, undefined, true); + }); + + test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async function () { + const folderPath = join(testDir, 'not-found'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + await Promises.mkdir(folderPath); + await changeFuture; + await Event.toPromise(watcher.onDidWatch); + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, undefined, 1); + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); + await Promises.rmdir(folderPath); + await changeFuture; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + await Promises.mkdir(folderPath); + await changeFuture; + await Event.toPromise(watcher.onDidWatch); + + await basicCrudTest(filePath, undefined, 1); + }); + + test('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () { + const folderPath = join(testDir, 'deep'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, undefined, 1); + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); + await Promises.rm(folderPath); + await changeFuture; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + await Promises.mkdir(folderPath); + await changeFuture; + await Event.toPromise(watcher.onDidWatch); + + await basicCrudTest(filePath, undefined, 1); + }); }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 42e8b4ad730..d01abbc9cf1 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -20,6 +20,7 @@ import { FileAccess } from 'vs/base/common/network'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { Emitter, Event } from 'vs/base/common/event'; // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in @@ -30,6 +31,11 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; class TestParcelWatcher extends ParcelWatcher { + protected override readonly missingRequestPathPollingInterval = 100; + + private readonly _onDidWatch = this._register(new Emitter()); + readonly onDidWatch = this._onDidWatch.event; + testNormalizePaths(paths: string[], excludes: string[] = []): string[] { // Work with strings as paths to simplify testing @@ -40,9 +46,11 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; return this.normalizeRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); } - override async watch(requests: IRecursiveWatchRequest[]): Promise { - await super.watch(requests); + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { + await super.doWatch(requests); await this.whenReady(); + + this._onDidWatch.fire(); } async whenReady(): Promise { @@ -646,4 +654,50 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3); await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3); }); + + test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => { + const folderPath = join(testDir, 'not-found'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + let onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.rm(folderPath); + await changeFuture; + await onDidWatch; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + }); + + test('correlated watch requests support suspend/resume (folder, exist in beginning)', async () => { + const folderPath = join(testDir, 'deep'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); + let onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.rm(folderPath); + await changeFuture; + await onDidWatch; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + }); }); From fffd7f14ace850eb79c1f667ef633a2fc2193362 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 08:02:22 -0800 Subject: [PATCH 080/141] simplify selector --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0e5593f71ab..4eb09223731 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -143,8 +143,7 @@ function parseCodeFromBlock(block?: string): string | undefined { return match ? match[1].trim() : undefined; } - const enum ChatElementSelectors { - ResponseEditor = 'div.chatMessageContent .interactive-result-editor .inputarea.monaco-mouse-cursor-text', + ResponseEditor = '.chatMessageContent textarea', ResponseMessage = '.chatMessageContent', } From cacdb7d87f138e66bc09d666c5395242bcbe6363 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 8 Mar 2024 13:05:27 -0300 Subject: [PATCH 081/141] Use 'command?' to match ChatResponseTurn (#207170) --- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 0f872032de2..351cf42e267 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -28,7 +28,7 @@ declare module 'vscode' { /** * The name of the {@link ChatCommand command} that was selected for this request. */ - readonly command: string | undefined; + readonly command?: string; /** * The variables that were referenced in this message. From 43558b2a2d56cf1582fe53324939362a11f96279 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:13:15 +0100 Subject: [PATCH 082/141] Activity bar top theme colors (#207172) --- .../lib/stylelint/vscode-known-variables.json | 2 ++ .../browser/parts/media/paneCompositePart.css | 12 +++++++---- src/vs/workbench/common/theme.ts | 21 +++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index a965cfcdb9f..a3b6d090d94 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -15,6 +15,7 @@ "--vscode-activityBarTop-dropBorder", "--vscode-activityBarTop-foreground", "--vscode-activityBarTop-inactiveForeground", + "--vscode-activityBarTop-background", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -560,6 +561,7 @@ "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", "--vscode-sideBarTitle-foreground", + "--vscode-sideBarActivityBarTop-border", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index e3502c3af86..332f9489175 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,12 +20,16 @@ display: none; } -.monaco-workbench .pane-composite-part > .header-or-footer.header { - border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border); +.monaco-workbench .pane-composite-part > .header-or-footer { + background-color: var(--vscode-activityBarTop-background); } -.monaco-workbench .pane-composite-part > .header-or-footer.footer { - border-top: 1px solid var(--vscode-sideBarSectionHeader-border); +.monaco-workbench .pane-composite-part > .header { + border-bottom: 1px solid var(--vscode-sideBarActivityBarTop-border); +} + +.monaco-workbench .pane-composite-part > .footer { + border-top: 1px solid var(--vscode-sideBarActivityBarTop-border); } .monaco-workbench .pane-composite-part > .title > .composite-bar-container, diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index febf755414e..651ddce192c 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -700,28 +700,35 @@ export const ACTIVITY_BAR_TOP_FOREGROUND = registerColor('activityBarTop.foregro light: '#424242', hcDark: Color.white, hcLight: editorForeground -}, localize('activityBarTop', "Active foreground color of the item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTop', "Active foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_ACTIVE_BORDER = registerColor('activityBarTop.activeBorder', { dark: ACTIVITY_BAR_TOP_FOREGROUND, light: ACTIVITY_BAR_TOP_FOREGROUND, hcDark: contrastBorder, hcLight: '#B5200D' -}, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTop.inactiveForeground', { dark: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.6), light: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.75), hcDark: Color.white, hcLight: editorForeground -}, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', { dark: ACTIVITY_BAR_TOP_FOREGROUND, light: ACTIVITY_BAR_TOP_FOREGROUND, hcDark: ACTIVITY_BAR_TOP_FOREGROUND, hcLight: ACTIVITY_BAR_TOP_FOREGROUND -}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); + +export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', { + dark: null, + light: null, + hcDark: null, + hcLight: null, +}, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); // < --- Profiles --- > @@ -871,6 +878,12 @@ export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeade hcLight: contrastBorder }, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', { + dark: SIDE_BAR_SECTION_HEADER_BORDER, + light: SIDE_BAR_SECTION_HEADER_BORDER, + hcDark: SIDE_BAR_SECTION_HEADER_BORDER, + hcLight: SIDE_BAR_SECTION_HEADER_BORDER +}, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); // < --- Title Bar --- > From b2bdefefd9d2245e3ba3418eda9e38d80f722762 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 8 Mar 2024 08:20:20 -0800 Subject: [PATCH 083/141] Make sure paste as html is disabled for implicit pastes (#207173) --- .../editor/contrib/dropOrPasteInto/browser/defaultProviders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index ca2d17065c3..5c86f71a94f 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -168,7 +168,7 @@ class PasteHtmlProvider implements DocumentPasteEditProvider { private readonly _yieldTo = [{ mimeType: Mimes.text }]; async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { - if (context.triggerKind !== DocumentPasteTriggerKind.PasteAs && context.only?.contains(this.kind)) { + if (context.triggerKind !== DocumentPasteTriggerKind.PasteAs && !context.only?.contains(this.kind)) { return; } From 465c98e0f2e9a08ad9bf3697b7e26a1f346ff677 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 08:50:00 -0800 Subject: [PATCH 084/141] fix vertical position --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 6 ++---- .../chat/browser/terminalChatController.ts | 2 ++ .../terminalContrib/chat/browser/terminalChatWidget.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index d923b51fa6d..a22490bbfda 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -89,8 +89,7 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation', - when: ContextKeyExpr.not('terminalChatFocus') + group: 'navigation' } }); } @@ -357,8 +356,7 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - isHiddenByDefault: true, - when: ContextKeyExpr.not('terminalChatFocus') + isHiddenByDefault: true } }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index cae129aca56..509d0867e73 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -288,6 +288,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id, providerId: 'terminal' }); + // the message grows in height, be sure to update top position so it doesn't go below the terminal + this._chatWidget?.value.layoutVertically(); const containsCode = responseContent.includes('```'); this._responseTypeContextKey.set(containsCode ? TerminalChatResponseTypes.TerminalCommand : TerminalChatResponseTypes.Message); this._chatWidget?.value.inlineChatWidget.updateToolbar(true); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4eb09223731..1743043d9e3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -68,6 +68,12 @@ export class TerminalChatWidget extends Disposable { this._focusedContextKey.set(true); this._visibleContextKey.set(true); this._inlineChatWidget.focus(); + this.layoutVertically(); + this._updateWidth(); + this._register(this._instance.onDimensionsChanged(() => this._updateWidth())); + } + + layoutVertically(): void { const font = this._instance.xterm?.getFont(); if (!font?.charHeight) { return; @@ -80,8 +86,6 @@ export class TerminalChatWidget extends Disposable { if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { this._container.style.top = ''; } - this._updateWidth(); - this._register(this._instance.onDimensionsChanged(() => this._updateWidth())); } private _updateWidth() { From 673b05c784f0f5ca6b3e161b8acd040c76d40c78 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 08:56:29 -0800 Subject: [PATCH 085/141] simplify context key --- .../chat/browser/terminalChat.ts | 8 ++--- .../chat/browser/terminalChatActions.ts | 31 +++++++++---------- .../chat/browser/terminalChatController.ts | 10 +++--- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 48fa65a5f50..1b54cf3fd62 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -36,15 +36,11 @@ export const enum TerminalChatContextKeyStrings { ChatInputHasText = 'terminalChatInputHasText', ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', - ChatResponseType = 'terminalChatResponseType', + ChatResponseContainsCodeBlock = 'terminalChatResponseContainsCodeBlock', ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', ChatSessionResponseVote = 'terminalChatSessionResponseVote', } -export const enum TerminalChatResponseTypes { - Message = 'message', - TerminalCommand = 'terminalCommand' -} export namespace TerminalChatContextKeys { @@ -67,7 +63,7 @@ export namespace TerminalChatContextKeys { export const responseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); /** The type of chat response, if any */ - export const responseType = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); + export const responseContainsCodeBlock = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block.")); /** Whether the response supports issue reporting */ export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index e0ba2d732c7..36e07a4f19a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -14,7 +14,7 @@ import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PRO import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ @@ -127,15 +127,14 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 2, - when: ContextKeyExpr.and(TerminalChatContextKeys.focused, - TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.responseContainsCodeBlock) }, f1: true, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.focused, - TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.responseContainsCodeBlock ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -156,7 +155,7 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.agentRegistered, - TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.responseContainsCodeBlock ), icon: Codicon.check, keybinding: { @@ -169,7 +168,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -189,7 +188,7 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.agentRegistered, - TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.responseContainsCodeBlock ), icon: Codicon.check, keybinding: { @@ -201,7 +200,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -226,13 +225,13 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.negate(), TerminalChatContextKeys.requestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, group: 'main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -302,7 +301,7 @@ registerActiveXtermAction({ title: localize2('feedbackHelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.responseType.notEqualsTo(undefined) + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined) ), icon: Codicon.thumbsup, toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('up'), @@ -310,7 +309,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 1, - when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -326,7 +325,7 @@ registerActiveXtermAction({ title: localize2('feedbackUnhelpful', 'Unhelpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.responseType.notEqualsTo(undefined), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), ), toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('down'), icon: Codicon.thumbsdown, @@ -334,7 +333,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 2, - when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -351,13 +350,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.responseType.notEqualsTo(undefined), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting ), icon: Codicon.report, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), group: 'inline', order: 3 }], diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 509d0867e73..6a601c42313 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -23,7 +23,7 @@ import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/te import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { MarkdownString } from 'vs/base/common/htmlContent'; const enum Message { @@ -63,7 +63,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey: IContextKey; private readonly _terminalAgentRegisteredContextKey: IContextKey; - private readonly _responseTypeContextKey: IContextKey; + private readonly _responseContainsCodeBlockContextKey: IContextKey; private readonly _responseSupportsIssueReportingContextKey: IContextKey; private readonly _sessionResponseVoteContextKey: IContextKey; @@ -101,7 +101,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); - this._responseTypeContextKey = TerminalChatContextKeys.responseType.bindTo(this._contextKeyService); + this._responseContainsCodeBlockContextKey = TerminalChatContextKeys.responseContainsCodeBlock.bindTo(this._contextKeyService); this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); @@ -212,7 +212,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._model.clear(); this._chatWidget?.value.hide(); this._chatWidget?.value.setValue(undefined); - this._responseTypeContextKey.reset(); + this._responseContainsCodeBlockContextKey.reset(); this._sessionResponseVoteContextKey.reset(); this._requestActiveContextKey.reset(); } @@ -291,7 +291,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr // the message grows in height, be sure to update top position so it doesn't go below the terminal this._chatWidget?.value.layoutVertically(); const containsCode = responseContent.includes('```'); - this._responseTypeContextKey.set(containsCode ? TerminalChatResponseTypes.TerminalCommand : TerminalChatResponseTypes.Message); + this._responseContainsCodeBlockContextKey.set(containsCode); this._chatWidget?.value.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } From 2974c9b49c72967184bffdf6ebc0361cd5275eb0 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:13:29 -0800 Subject: [PATCH 086/141] Fix name (#207181) --- src/vs/workbench/contrib/speech/common/speechService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index 6fdd365f39c..6aced99f16e 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -168,7 +168,8 @@ export const SPEECH_LANGUAGES = { name: localize('speechLanguage.sv-SE', "Swedish (Sweden)") }, ['tr-TR']: { - name: localize('speechLanguage.tr-TR', "Turkish (Turkey)") + // allow-any-unicode-next-line + name: localize('speechLanguage.tr-TR', "Turkish (Türkiye)") }, ['zh-CN']: { name: localize('speechLanguage.zh-CN', "Chinese (Simplified, China)") From 42ddf6570308290a5d825e6347ffab223f147045 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 8 Mar 2024 11:30:40 -0800 Subject: [PATCH 087/141] Force walkthrough image rebuild on selectStep (#207183) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index a34712f15cb..725248d0031 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -511,13 +511,13 @@ export class GettingStartedPage extends EditorPane { private currentMediaComponent: string | undefined = undefined; private currentMediaType: string | undefined = undefined; - private async buildMediaComponent(stepId: string) { + private async buildMediaComponent(stepId: string, forceRebuild: boolean = false) { if (!this.currentWalkthrough) { throw Error('no walkthrough selected'); } const stepToExpand = assertIsDefined(this.currentWalkthrough.steps.find(step => step.id === stepId)); - if (this.currentMediaComponent === stepId) { return; } + if (!forceRebuild && this.currentMediaComponent === stepId) { return; } this.currentMediaComponent = stepId; this.stepDisposables.clear(); @@ -726,7 +726,7 @@ export class GettingStartedPage extends EditorPane { stepElement.classList.add('expanded'); stepElement.setAttribute('aria-expanded', 'true'); - this.buildMediaComponent(id); + this.buildMediaComponent(id, true); this.gettingStartedService.progressByEvent('stepSelected:' + id); } else { this.editorInput.selectedStep = undefined; From e5393d4e0b6a9464a773bcff29b48024bbca0f20 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 8 Mar 2024 20:42:16 +0100 Subject: [PATCH 088/141] fix #207177 (#207185) --- .../extensions/browser/extensionsActions.ts | 14 +++++++++---- .../browser/extensionsWorkbenchService.ts | 20 ++++++++++--------- .../contrib/extensions/common/extensions.ts | 1 + .../extensionsActions.test.ts | 4 ++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 43e8a7c086d..9768c107db0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1562,6 +1562,7 @@ export class ExtensionRuntimeStateAction extends ExtensionAction { updateWhenCounterExtensionChanges: boolean = true; constructor( + @IHostService private readonly hostService: IHostService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, @@ -1598,15 +1599,20 @@ export class ExtensionRuntimeStateAction extends ExtensionAction { this.enabled = true; this.class = ExtensionRuntimeStateAction.EnabledClass; this.tooltip = runtimeState.reason; - this.label = runtimeState.action === ExtensionRuntimeActionType.RestartExtensions ? localize('restart extensions', 'Restart Extensions') - : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart to Update') - : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; + this.label = runtimeState.action === ExtensionRuntimeActionType.ReloadWindow ? localize('reload window', 'Reload Window') + : runtimeState.action === ExtensionRuntimeActionType.RestartExtensions ? localize('restart extensions', 'Restart Extensions') + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart to Update') + : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } override async run(): Promise { const runtimeState = this.extension?.runtimeState; - if (runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions) { + if (runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow) { + return this.hostService.reload(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions) { return this.extensionsWorkbenchService.updateRunningExtensions(); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ec74cf72646..5846f2bb915 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1147,12 +1147,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); + const reloadAction = this.extensionManagementServerService.remoteExtensionManagementServer ? ExtensionRuntimeActionType.ReloadWindow : ExtensionRuntimeActionType.RestartExtensions; + const reloadActionLabel = reloadAction === ExtensionRuntimeActionType.ReloadWindow ? nls.localize('reload', "reload window") : nls.localize('restart extensions', "restart extensions"); if (isUninstalled) { const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postUninstallTooltip', "Please restart extensions to complete the uninstallation of this extension.") }; + return { action: reloadAction, reason: nls.localize('postUninstallTooltip', "Please {0} to complete the uninstallation of this extension.", reloadActionLabel) }; } return undefined; } @@ -1190,7 +1192,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } return undefined; } - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postUpdateTooltip', "Please restart extensions to enable the updated extension.") }; + return { action: reloadAction, reason: nls.localize('postUpdateTooltip', "Please {0} to enable the updated extension.", reloadActionLabel) }; } if (this.extensionsServers.length > 1) { @@ -1198,12 +1200,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('enable locally', "Please restart extensions to enable this extension locally.") }; + return { action: reloadAction, reason: nls.localize('enable locally', "Please {0} to enable this extension locally.", reloadActionLabel) }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('enable remote', "Please restart extensions to enable this extension in {1}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; + return { action: reloadAction, reason: nls.localize('enable remote', "Please {0} to enable this extension in {1}.", reloadActionLabel, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1213,20 +1215,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } } return undefined; } else { if (isSameExtensionRunning) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postDisableTooltip', "Please restart extensions to disable this extension.") }; + return { action: reloadAction, reason: nls.localize('postDisableTooltip', "Please {0} to disable this extension.", reloadActionLabel) }; } } return undefined; @@ -1235,7 +1237,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1243,7 +1245,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return { action: ExtensionRuntimeActionType.RestartExtensions, reason: nls.localize('postEnableTooltip', "Please restart extensions to enable this extension.") }; + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 8be5639f119..8bae2eeae25 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -40,6 +40,7 @@ export const enum ExtensionState { } export const enum ExtensionRuntimeActionType { + ReloadWindow = 'reloadWindow', RestartExtensions = 'restartExtensions', DownloadUpdate = 'downloadUpdate', ApplyUpdate = 'applyUpdate', diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index f8c7f267f13..f7c858e86d9 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -1441,7 +1441,7 @@ suite('ExtensionRuntimeStateAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); + assert.strictEqual(testObject.tooltip, `Please reload window to enable this extension.`); }); test('Test Runtime State when ui extension is disabled on remote server and installed in local server', async () => { @@ -1480,7 +1480,7 @@ suite('ExtensionRuntimeStateAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); + assert.strictEqual(testObject.tooltip, `Please reload window to enable this extension.`); }); test('Test Runtime State for remote ui extension is disabled when it is installed and enabled in local server', async () => { From d6883e6b4b80e4c97bd819dd5abafd2ff4a645bf Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 12:27:22 -0800 Subject: [PATCH 089/141] alternate approach --- .../chat/browser/media/terminalChatWidget.css | 4 +++ .../chat/browser/terminalChatActions.ts | 25 ++++++++++++------- .../chat/browser/terminalChatWidget.ts | 21 ++++++++-------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index ad8623e881c..3e2b58d2d41 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -22,3 +22,7 @@ .terminal-inline-chat .chatMessageContent { width: 400px !important; } + +.interactive-result-code-block-toolbar { + margin-top: 7px; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 36e07a4f19a..2d30d5519fa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -6,6 +6,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize2 } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; @@ -165,10 +166,13 @@ registerActiveXtermAction({ }, menu: { // TODO: Allow action to be made primary, the action list is hardcoded within InlineChatWidget - id: MENU_TERMINAL_CHAT_WIDGET_STATUS, - group: '0_main', - order: 0, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + // id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + // group: '0_main', + // order: 0, + // when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + id: MenuId.ChatCodeBlock, + group: 'navigation', + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -190,17 +194,20 @@ registerActiveXtermAction({ TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock ), - icon: Codicon.check, + icon: Codicon.pencil, keybinding: { when: TerminalChatContextKeys.requestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Alt | KeyCode.Enter, }, menu: { - id: MENU_TERMINAL_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + // id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + // group: '0_main', + // order: 1, + // when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + id: MenuId.ChatCodeBlock, + group: 'navigation', + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 1743043d9e3..950d54fcd5d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -6,6 +6,7 @@ import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/terminalChatWidget'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -30,7 +31,8 @@ export class TerminalChatWidget extends Disposable { terminalElement: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(); @@ -126,12 +128,16 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.value = value ?? ''; } acceptCommand(shouldExecute: boolean): void { - // Trim command to remove any whitespace, otherwise this may execute the command - const value = parseCodeFromBlock(this._inlineChatWidget?.responseContent?.trim()); - if (!value) { + const editor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor(); + if (!editor) { return; } - this._instance.runCommand(value, shouldExecute); + const model = editor.getModel(); + if (!model) { + return; + } + const code = editor.getValue(); + this._instance.runCommand(code, shouldExecute); this.hide(); } updateProgress(progress?: IChatProgress): void { @@ -142,11 +148,6 @@ export class TerminalChatWidget extends Disposable { } } -function parseCodeFromBlock(block?: string): string | undefined { - const match = block?.match(/```.*?\n([\s\S]*?)```/); - return match ? match[1].trim() : undefined; -} - const enum ChatElementSelectors { ResponseEditor = '.chatMessageContent textarea', ResponseMessage = '.chatMessageContent', From c7deee7fc12daf1888e253a4f0345d46bf4a510b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 12:36:40 -0800 Subject: [PATCH 090/141] fix css --- .../terminalContrib/chat/browser/media/terminalChatWidget.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 3e2b58d2d41..e0a0467f2d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -23,6 +23,6 @@ width: 400px !important; } -.interactive-result-code-block-toolbar { - margin-top: 7px; +.terminal-inline-chat .chatMessageContent .value { + padding-top: 10px; } From e7e73cad5fc8bb2f8d98d56f685dd29a707e6df5 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 12:44:32 -0800 Subject: [PATCH 091/141] tweak --- .../chat/browser/terminalChatActions.ts | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 2d30d5519fa..af3ffe462fb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -158,18 +158,14 @@ registerActiveXtermAction({ TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock ), - icon: Codicon.check, + icon: Codicon.play, keybinding: { when: TerminalChatContextKeys.requestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { - // TODO: Allow action to be made primary, the action list is hardcoded within InlineChatWidget - // id: MENU_TERMINAL_CHAT_WIDGET_STATUS, - // group: '0_main', - // order: 0, - // when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + order: 0, id: MenuId.ChatCodeBlock, group: 'navigation', when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) @@ -194,19 +190,15 @@ registerActiveXtermAction({ TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock ), - icon: Codicon.pencil, keybinding: { when: TerminalChatContextKeys.requestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Alt | KeyCode.Enter, }, menu: { - // id: MENU_TERMINAL_CHAT_WIDGET_STATUS, - // group: '0_main', - // order: 1, - // when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), id: MenuId.ChatCodeBlock, group: 'navigation', + isHiddenByDefault: true, when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) }, run: (_xterm, _accessor, activeInstance) => { @@ -367,12 +359,6 @@ registerActiveXtermAction({ group: 'inline', order: 3 }], - // { - // id: MENU_TERMINAL_CHAT_WIDGET, - // when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), TerminalChatContextKeys.chatResponseSupportsIssueReporting), - // group: 'config', - // order: 3 - // }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; From 3c2a5e49b7fbd88efc7c52f79a861132a112b603 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 12:55:42 -0800 Subject: [PATCH 092/141] terminal chat context --- .../chat/browser/actions/chatCodeblockActions.ts | 12 +++++++++++- .../terminalContrib/chat/browser/terminalChat.ts | 3 --- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index a22490bbfda..93cdb83d037 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -20,7 +20,7 @@ import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; @@ -565,6 +565,7 @@ export function registerChatCodeBlockActions() { function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); const accessibleViewService = accessor.get(IAccessibleViewService); + const contextKeyService = accessor.get(IContextKeyService); const accessibleViewCodeBlock = accessibleViewService.getCodeBlockContext(); if (accessibleViewCodeBlock) { return accessibleViewCodeBlock; @@ -574,6 +575,15 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): return; } + if (contextKeyService.getContext(editor.getDomNode()).getValue('terminalChatFocus')) { + return { + element: editor, + code: editor.getValue(), + codeBlockIndex: 0, + languageId: editor.getModel()!.getLanguageId() + }; + } + const widget = chatWidgetService.lastFocusedWidget; if (!widget) { return; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 1b54cf3fd62..839bdbd75d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -59,9 +59,6 @@ export namespace TerminalChatContextKeys { /** Whether the terminal chat agent has been registered */ export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); - /** Whether the chat response editor is focused */ - export const responseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); - /** The type of chat response, if any */ export const responseContainsCodeBlock = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block.")); From 1de50c8f6fa64cd9529594c653eef8b0b0864fc0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 13:11:18 -0800 Subject: [PATCH 093/141] allow code block to be editable --- .../contrib/chat/browser/chatListRenderer.ts | 3 ++- .../contrib/chat/browser/codeBlockPart.ts | 6 +++--- .../inlineChat/browser/inlineChatWidget.ts | 21 ++++++++++++------- .../chat/browser/terminalChatWidget.ts | 1 + 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 37b54b0c01f..0b719131603 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -101,6 +101,7 @@ export interface IChatListItemRendererOptions { readonly renderStyle?: 'default' | 'compact'; readonly noHeader?: boolean; readonly noPadding?: boolean; + readonly editableCodeBlock?: boolean; } export class ChatListItemRenderer extends Disposable implements ITreeRenderer { @@ -937,7 +938,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { const ref = this._editorPool.get(); const editorInfo = ref.object; - editorInfo.render(data, this._currentLayoutWidth); + editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); return ref; } diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index a39f69dbac8..f188f47bef3 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -113,7 +113,7 @@ export interface ICodeBlockPart { readonly element: HTMLElement; readonly uri: URI | undefined; layout(width: number): void; - render(data: ICodeBlockData, width: number): Promise; + render(data: ICodeBlockData, width: number, editable?: boolean): Promise; focus(): void; reset(): unknown; dispose(): void; @@ -331,7 +331,7 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart { return this.editor.getContentHeight(); } - async render(data: ICodeBlockData, width: number) { + async render(data: ICodeBlockData, width: number, editable: boolean) { if (data.parentContextKeyService) { this.contextKeyService.updateParent(data.parentContextKeyService); } @@ -345,7 +345,7 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart { await this.updateEditor(data); this.layout(width); - this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1) }); + this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1), readOnly: !editable }); if (data.hideToolbar) { dom.hide(this.toolbar.getElement()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index fb7f2c67237..8e729d360ae 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -97,6 +97,11 @@ export interface IInlineChatWidgetConstructionOptions { * The men that rendered in the lower right corner, use for feedback */ feedbackMenuId: MenuId; + + /** + * Whether the code blocks are editable. + */ + editableCodeBlock?: boolean; } export interface IInlineChatMessage { @@ -172,7 +177,7 @@ export class InlineChatWidget { private readonly _codeBlockModelCollection: CodeBlockModelCollection; constructor( - options: IInlineChatWidgetConstructionOptions, + private readonly _options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -187,7 +192,7 @@ export class InlineChatWidget { const hoverDelegate = this._store.add(createInstantHoverDelegate()); // input editor logic - this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: options.inputMenuId, telemetrySource: options.telemetrySource, hoverDelegate }); + this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: _options.inputMenuId, telemetrySource: _options.telemetrySource, hoverDelegate }); this._elements.body.replaceChild(this._inputWidget.domNode, this._elements.content); this._store.add(this._inputWidget); this._store.add(this._inputWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); @@ -219,15 +224,15 @@ export class InlineChatWidget { this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { - telemetrySource: options.telemetrySource, + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { + telemetrySource: _options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; - const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu; + const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options; const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); @@ -242,7 +247,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -373,7 +378,7 @@ export class InlineChatWidget { const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); - const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; + const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true, editableCodeBlock: this._options.editableCodeBlock }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 950d54fcd5d..26af5678964 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -51,6 +51,7 @@ export class TerminalChatWidget extends Disposable { statusMenuId: MENU_TERMINAL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, telemetrySource: 'terminal-inline-chat', + editableCodeBlock: true } ); this._reset(); From 016fa658d8bcfad1175ca0e508d20229c17151c1 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 13:41:56 -0800 Subject: [PATCH 094/141] add provider registry --- .../browser/actions/chatCodeblockActions.ts | 44 ++++++++++++++----- .../chat/browser/terminalChatController.ts | 18 ++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 93cdb83d037..060b9b0cdb3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -6,6 +6,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; @@ -20,7 +21,9 @@ import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; @@ -562,10 +565,32 @@ export function registerChatCodeBlockActions() { }); } +export interface ICodeBlockActionContextProvider { + getCodeBlockContext(editor: ICodeEditor): ICodeBlockActionContext | undefined; +} +export const ICodeBlockContextProviderRegistry = createDecorator('codeBlockContextProviderRegistry'); +export interface ICodeBlockContextProviderRegistry { +} +export class CodeBlockContextProviderRegistry { + serviceBrand: undefined; + static providers = new Map(); + static getProviders(): ICodeBlockActionContextProvider[] { + return [...CodeBlockContextProviderRegistry.providers.values()]; + } + static registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { + CodeBlockContextProviderRegistry.providers.set(id, provider); + return toDisposable(() => CodeBlockContextProviderRegistry.providers.delete(id)); + } + + getProvider(id: string): ICodeBlockActionContextProvider | undefined { + return CodeBlockContextProviderRegistry.providers.get(id); + } +} +registerSingleton(ICodeBlockContextProviderRegistry, CodeBlockContextProviderRegistry, InstantiationType.Delayed); + function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); const accessibleViewService = accessor.get(IAccessibleViewService); - const contextKeyService = accessor.get(IContextKeyService); const accessibleViewCodeBlock = accessibleViewService.getCodeBlockContext(); if (accessibleViewCodeBlock) { return accessibleViewCodeBlock; @@ -575,17 +600,14 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): return; } - if (contextKeyService.getContext(editor.getDomNode()).getValue('terminalChatFocus')) { - return { - element: editor, - code: editor.getValue(), - codeBlockIndex: 0, - languageId: editor.getModel()!.getLanguageId() - }; - } - const widget = chatWidgetService.lastFocusedWidget; if (!widget) { + for (const provider of CodeBlockContextProviderRegistry.getProviders()) { + const context = provider.getCodeBlockContext(editor); + if (context) { + return context; + } + } return; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 6a601c42313..44bbdcce1ed 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -25,6 +25,7 @@ import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/br import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { CodeBlockContextProviderRegistry } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; const enum Message { NONE = 0, @@ -118,6 +119,23 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } + CodeBlockContextProviderRegistry.registerProvider({ + getCodeBlockContext: (editor) => { + const chatWidget = this.chatWidget; + if (!chatWidget) { + return; + } + if (!editor) { + return; + } + return { + element: editor, + code: editor.getValue(), + codeBlockIndex: 0, + languageId: editor.getModel()!.getLanguageId() + }; + } + }, 'terminal'); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { From 93f537f06f8084f018578eee0983d31041bad168 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 8 Mar 2024 23:26:11 +0100 Subject: [PATCH 095/141] dispose RangeProvider after usage (#207207) --- .../browser/stickyScrollModelProvider.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index f1f50f9ced7..7f7b99c5a9d 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -397,9 +397,13 @@ class StickyModelFromCandidateIndentationFoldingProvider extends StickyModelFrom return null; } - protected createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { + protected async createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { const provider = new IndentRangeProvider(textModel, this._languageConfigurationService, this._foldingLimitReporter); - return provider.compute(token); + try { + return await provider.compute(token); + } finally { + provider.dispose(); + } } } @@ -422,6 +426,10 @@ class StickyModelFromCandidateSyntaxFoldingProvider extends StickyModelFromCandi protected createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, textModel); const provider = new SyntaxRangeProvider(textModel, selectedProviders, () => this.createModelFromProvider(textModel, modelVersionId, token), this._foldingLimitReporter, undefined); - return provider.compute(token); + try { + return provider.compute(token); + } finally { + provider.dispose(); + } } } From cf4c04e7fd1149bd7df45925b32239d68b58289b Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 8 Mar 2024 15:13:55 -0800 Subject: [PATCH 096/141] Add the new API endpoint overload to include error location for diagnostics (#207208) * new API overload to provide an error location in a notebook cell * use IRange * more error details all in one place --- .../api/common/extHostNotebookKernels.ts | 17 ++++++- .../notebookExecutionStateServiceImpl.ts | 1 + .../contrib/notebook/common/notebookCommon.ts | 2 + .../common/notebookExecutionStateService.ts | 10 ++++- .../common/extensionsApiProposals.ts | 1 + ...vscode.proposed.notebookCellExecution.d.ts | 44 +++++++++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 998e624a195..a52ad806577 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -702,7 +702,7 @@ class NotebookCellExecutionTask extends Disposable { }); }, - end(success: boolean | undefined, endTime?: number): void { + end(success: boolean | undefined, endTime?: number, executionError?: vscode.CellExecutionError): void { if (that._state === NotebookCellExecutionTaskState.Resolved) { throw new Error('Cannot call resolve twice'); } @@ -714,9 +714,22 @@ class NotebookCellExecutionTask extends Disposable { // so we use updateSoon and immediately flush. that._collector.flush(); + const error = executionError ? { + message: executionError.message, + stack: executionError.stack, + location: executionError?.location ? { + startLineNumber: executionError.location.start.line, + startColumn: executionError.location.start.character, + endLineNumber: executionError.location.end.line, + endColumn: executionError.location.end.character + } : undefined, + uri: executionError.uri + } : undefined; + that._proxy.$completeExecution(that._handle, new SerializableObjectWithBuffers({ runEndTime: endTime, - lastRunSuccess: success + lastRunSuccess: success, + error })); }, diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index dbc9e88e9a4..52157f4ae29 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts @@ -527,6 +527,7 @@ class CellExecution extends Disposable implements INotebookCellExecution { lastRunSuccess: completionData.lastRunSuccess, runStartTime: this._didPause ? null : cellModel.internalMetadata.runStartTime, runEndTime: this._didPause ? null : completionData.runEndTime, + error: completionData.error } }; this._applyExecutionEdits([edit]); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c673ae8ad79..02c59967a70 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -32,6 +32,7 @@ import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/serv import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IFileReadLimits } from 'vs/platform/files/common/files'; import { parse as parseUri, generate as generateUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; +import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; @@ -120,6 +121,7 @@ export interface NotebookCellInternalMetadata { runStartTimeAdjustment?: number; runEndTime?: number; renderDuration?: { [key: string]: number }; + error?: ICellExecutionError; } export interface NotebookCellCollapseState { diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index 98a24d28adf..5b98e7ca262 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -5,7 +5,8 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IRange } from 'vs/editor/common/core/range'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { NotebookCellExecutionState, NotebookExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, ICellExecuteOutputEdit, ICellExecuteOutputItemEdit } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -20,9 +21,16 @@ export interface ICellExecutionStateUpdate { isPaused?: boolean; } +export interface ICellExecutionError { + message: string; + stack: string | undefined; + uri: UriComponents; + location: IRange | undefined; +} export interface ICellExecutionComplete { runEndTime?: number; lastRunSuccess?: boolean; + error?: ICellExecutionError; } export enum NotebookExecutionType { cell, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 67c26906df3..548f66575c0 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -77,6 +77,7 @@ export const allApiProposals = Object.freeze({ mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', multiDocumentHighlightProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', newSymbolNamesProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', + notebookCellExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', diff --git a/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts b/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts new file mode 100644 index 00000000000..944f68f5fe3 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface NotebookCellExecution { + /** + * Signal that execution has ended. + * + * @param success If true, a green check is shown on the cell status bar. + * If false, a red X is shown. + * If undefined, no check or X icon is shown. + * @param endTime The time that execution finished, in milliseconds in the Unix epoch. + * @param error Details about an error that occurred during execution if any. + */ + end(success: boolean | undefined, endTime?: number, error?: CellExecutionError): void; + } + + export interface CellExecutionError { + /** + * The error message. + */ + readonly message: string; + + /** + * The error stack trace. + */ + readonly stack: string | undefined; + + /** + * The cell resource which had the error. + */ + uri: Uri; + + /** + * The location within the resource where the error occurred. + */ + readonly location: Range | undefined; + + + } +} From 877a181582952eb2b2262a4663c81355a0e6bb4c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 8 Mar 2024 15:40:04 -0800 Subject: [PATCH 097/141] testing: fix proposed obsever API not rebuilding test tree (#207206) Use the new prefix tree implementation. --- src/vs/workbench/api/common/extHostTesting.ts | 3 +- .../api/common/extHostTypeConverters.ts | 53 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 5d0ca1cb8b9..dcae2c2c45c 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -13,7 +13,6 @@ import { createSingleCallFunction } from 'vs/base/common/functional'; import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -296,7 +295,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(r => deepFreeze(Convert.TestResults.to(r))) + .map(Convert.TestResults.to) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 13077072ff3..eb1e89cf5c6 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -14,8 +14,9 @@ import { marked } from 'vs/base/common/marked/marked'; import { parse, revive } from 'vs/base/common/marshalling'; import { Mimes } from 'vs/base/common/mime'; import { cloneAndChange } from 'vs/base/common/objects'; +import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { basename } from 'vs/base/common/resources'; -import { isEmptyObject, isNumber, isString, isUndefinedOrNull } from 'vs/base/common/types'; +import { isDefined, isEmptyObject, isNumber, isString, isUndefinedOrNull } from 'vs/base/common/types'; import { URI, UriComponents, isUriComponents } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -38,15 +39,15 @@ import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateA import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/editor'; import { IViewBadge } from 'vs/workbench/common/views'; import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTreeData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; +import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatCommandFollowup, IInlineChatFollowup, IInlineChatReplyFollowup, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestTag, TestMessageType, TestResultItem, denamespaceTestTag, namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -1956,18 +1957,15 @@ export namespace TestTag { } export namespace TestResults { - const convertTestResultItem = (item: TestResultItem.Serialized, byInternalId: Map): vscode.TestResultSnapshot => { - const children: TestResultItem.Serialized[] = []; - for (const [id, item] of byInternalId) { - if (TestId.compare(item.item.extId, id) === TestPosition.IsChild) { - byInternalId.delete(id); - children.push(item); - } + const convertTestResultItem = (node: IPrefixTreeNode, parent?: vscode.TestResultSnapshot): vscode.TestResultSnapshot | undefined => { + const item = node.value; + if (!item) { + return undefined; // should be unreachable } const snapshot: vscode.TestResultSnapshot = ({ ...TestItem.toPlain(item.item), - parent: undefined, + parent, taskStates: item.tasks.map(t => ({ state: t.state as number as types.TestResultState, duration: t.duration, @@ -1975,30 +1973,43 @@ export namespace TestResults { .filter((m): m is ITestErrorMessage.Serialized => m.type === TestMessageType.Error) .map(TestMessage.to), })), - children: children.map(c => convertTestResultItem(c, byInternalId)) + children: [], }); - for (const child of snapshot.children) { - (child as any).parent = snapshot; + if (node.children) { + for (const child of node.children.values()) { + const c = convertTestResultItem(child, snapshot); + if (c) { + snapshot.children.push(c); + } + } } return snapshot; }; export function to(serialized: ISerializedTestResults): vscode.TestRunResult { - const roots: TestResultItem.Serialized[] = []; - const byInternalId = new Map(); + const tree = new WellDefinedPrefixTree(); for (const item of serialized.items) { - byInternalId.set(item.item.extId, item); - const controllerId = TestId.root(item.item.extId); - if (serialized.request.targets.some(t => t.controllerId === controllerId && t.testIds.includes(item.item.extId))) { - roots.push(item); + tree.insert(TestId.fromString(item.item.extId).path, item); + } + + // Get the first node with a value in each subtree of IDs. + const queue = [tree.nodes]; + const roots: IPrefixTreeNode[] = []; + while (queue.length) { + for (const node of queue.pop()!) { + if (node.value) { + roots.push(node); + } else if (node.children) { + queue.push(node.children.values()); + } } } return { completedAt: serialized.completedAt, - results: roots.map(r => convertTestResultItem(r, byInternalId)), + results: roots.map(r => convertTestResultItem(r)).filter(isDefined), }; } } From dc97163e1c3ee9dac2cf3139dd1a02a882ee0645 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 8 Mar 2024 15:57:47 -0800 Subject: [PATCH 098/141] add accessible view provider --- .../accessibility/browser/accessibleView.ts | 4 + .../browser/actions/chatCodeblockActions.ts | 44 +---------- src/vs/workbench/contrib/chat/browser/chat.ts | 24 ++++++ .../chat/browser/chatAccessibilityProvider.ts | 79 +++++++++++++++++++ .../contrib/chat/browser/chatListRenderer.ts | 72 +---------------- .../contrib/chat/browser/chatWidget.ts | 3 +- .../chat/browser/terminalChatController.ts | 7 +- 7 files changed, 117 insertions(+), 116 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 86f6c05bbd1..e51a8bebeb1 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -42,6 +42,7 @@ import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQui import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { CodeBlockContextProviderRegistry } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -338,6 +339,9 @@ export class AccessibleView extends Disposable { // only cache a provider with an ID so that it will eventually be cleared. this._lastProvider = provider; } + if (provider.id === AccessibleViewProviderId.Chat) { + this._register(CodeBlockContextProviderRegistry.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView')); + } } previous(): void { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 060b9b0cdb3..2793bf70f13 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -6,7 +6,6 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; @@ -22,15 +21,12 @@ import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CodeBlockContextProviderRegistry, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; @@ -565,44 +561,17 @@ export function registerChatCodeBlockActions() { }); } -export interface ICodeBlockActionContextProvider { - getCodeBlockContext(editor: ICodeEditor): ICodeBlockActionContext | undefined; -} -export const ICodeBlockContextProviderRegistry = createDecorator('codeBlockContextProviderRegistry'); -export interface ICodeBlockContextProviderRegistry { -} -export class CodeBlockContextProviderRegistry { - serviceBrand: undefined; - static providers = new Map(); - static getProviders(): ICodeBlockActionContextProvider[] { - return [...CodeBlockContextProviderRegistry.providers.values()]; - } - static registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { - CodeBlockContextProviderRegistry.providers.set(id, provider); - return toDisposable(() => CodeBlockContextProviderRegistry.providers.delete(id)); - } - - getProvider(id: string): ICodeBlockActionContextProvider | undefined { - return CodeBlockContextProviderRegistry.providers.get(id); - } -} -registerSingleton(ICodeBlockContextProviderRegistry, CodeBlockContextProviderRegistry, InstantiationType.Delayed); - function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); - const accessibleViewService = accessor.get(IAccessibleViewService); - const accessibleViewCodeBlock = accessibleViewService.getCodeBlockContext(); - if (accessibleViewCodeBlock) { - return accessibleViewCodeBlock; - } const model = editor.getModel(); if (!model) { return; } const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - for (const provider of CodeBlockContextProviderRegistry.getProviders()) { + const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri); + if (!codeBlockInfo) { + for (const provider of CodeBlockContextProviderRegistry.providers) { const context = provider.getCodeBlockContext(editor); if (context) { return context; @@ -611,11 +580,6 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): return; } - const codeBlockInfo = widget.getCodeBlockInfoForEditor(model.uri); - if (!codeBlockInfo) { - return; - } - return { element: codeBlockInfo.element, codeBlockIndex: codeBlockInfo.codeBlockIndex, diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index daa5ac41eb1..ecc65df2abf 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -4,11 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; +import { registerSingleton, InstantiationType } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -16,6 +19,7 @@ import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWel export const IChatWidgetService = createDecorator('chatWidgetService'); export const IQuickChatService = createDecorator('quickChatService'); export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); +export const ICodeBlockContextProviderRegistry = createDecorator('codeBlockContextProviderRegistry'); export interface IChatWidgetService { @@ -134,3 +138,23 @@ export interface IChatWidget { export interface IChatViewPane { clear(): void; } + + +export interface ICodeBlockActionContextProvider { + getCodeBlockContext(editor?: ICodeEditor): ICodeBlockActionContext | undefined; +} +export interface ICodeBlockContextProviderRegistry { + serviceBrand: undefined; +} +export class CodeBlockContextProviderRegistry implements ICodeBlockContextProviderRegistry { + serviceBrand: undefined; + static readonly _providers = new Map(); + static get providers(): ICodeBlockActionContextProvider[] { + return [...this._providers.values()]; + } + static registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { + this._providers.set(id, provider); + return toDisposable(() => this._providers.delete(id)); + } +} +registerSingleton(ICodeBlockContextProviderRegistry, CodeBlockContextProviderRegistry, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts new file mode 100644 index 00000000000..933a8940869 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { marked } from 'vs/base/common/marked/marked'; +import { localize } from 'vs/nls'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatAccessibilityProvider implements IListAccessibilityProvider { + + constructor( + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + ) { + + } + getWidgetRole(): AriaRole { + return 'list'; + } + + getRole(element: ChatTreeItem): AriaRole | undefined { + return 'listitem'; + } + + getWidgetAriaLabel(): string { + return localize('chat', "Chat"); + } + + getAriaLabel(element: ChatTreeItem): string { + if (isRequestVM(element)) { + return element.messageText; + } + + if (isResponseVM(element)) { + return this._getLabelWithCodeBlockCount(element); + } + + if (isWelcomeVM(element)) { + return element.content.map(c => 'value' in c ? c.value : c.map(followup => followup.message).join('\n')).join('\n'); + } + + return ''; + } + + private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string { + const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.Chat); + let label: string = ''; + const fileTreeCount = element.response.value.filter((v) => !('value' in v))?.length ?? 0; + let fileTreeCountHint = ''; + switch (fileTreeCount) { + case 0: + break; + case 1: + fileTreeCountHint = localize('singleFileTreeHint', "1 file tree"); + break; + default: + fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees", fileTreeCount); + break; + } + const codeBlockCount = marked.lexer(element.response.asString()).filter(token => token.type === 'code')?.length ?? 0; + switch (codeBlockCount) { + case 0: + label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); + break; + case 1: + label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); + break; + default: + label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); + break; + } + return label; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0b719131603..f60b2549be8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -5,11 +5,10 @@ import * as dom from 'vs/base/browser/dom'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { AriaRole, alert } from 'vs/base/browser/ui/aria/aria'; +import { alert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; @@ -23,7 +22,6 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { marked } from 'vs/base/common/marked/marked'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; import { basename } from 'vs/base/common/path'; @@ -50,8 +48,6 @@ import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer, IMarkdownVulnerability, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; @@ -999,72 +995,6 @@ export class ChatListDelegate implements IListVirtualDelegate { } } -export class ChatAccessibilityProvider implements IListAccessibilityProvider { - - constructor( - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService - ) { - - } - getWidgetRole(): AriaRole { - return 'list'; - } - - getRole(element: ChatTreeItem): AriaRole | undefined { - return 'listitem'; - } - - getWidgetAriaLabel(): string { - return localize('chat', "Chat"); - } - - getAriaLabel(element: ChatTreeItem): string { - if (isRequestVM(element)) { - return element.messageText; - } - - if (isResponseVM(element)) { - return this._getLabelWithCodeBlockCount(element); - } - - if (isWelcomeVM(element)) { - return element.content.map(c => 'value' in c ? c.value : c.map(followup => followup.message).join('\n')).join('\n'); - } - - return ''; - } - - private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string { - const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.Chat); - let label: string = ''; - const fileTreeCount = element.response.value.filter((v) => !('value' in v))?.length ?? 0; - let fileTreeCountHint = ''; - switch (fileTreeCount) { - case 0: - break; - case 1: - fileTreeCountHint = localize('singleFileTreeHint', "1 file tree"); - break; - default: - fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees", fileTreeCount); - break; - } - const codeBlockCount = marked.lexer(element.response.asString()).filter(token => token.type === 'code')?.length ?? 0; - switch (codeBlockCount) { - case 0: - label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); - break; - case 1: - label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); - break; - default: - label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); - break; - } - return label; - } -} - interface IDisposableReference extends IDisposable { object: T; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b2e1680a28c..bcc50a4370c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -26,8 +26,9 @@ import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityProvider } from 'vs/workbench/contrib/chat/browser/chatAccessibilityProvider'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { ChatAccessibilityProvider, ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 44bbdcce1ed..88d02f60056 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -13,7 +13,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CodeBlockContextProviderRegistry, IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserAction } from 'vs/workbench/contrib/chat/common/chatService'; @@ -25,7 +25,6 @@ import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/br import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { CodeBlockContextProviderRegistry } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; const enum Message { NONE = 0, @@ -119,7 +118,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } - CodeBlockContextProviderRegistry.registerProvider({ + this._register(CodeBlockContextProviderRegistry.registerProvider({ getCodeBlockContext: (editor) => { const chatWidget = this.chatWidget; if (!chatWidget) { @@ -135,7 +134,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr languageId: editor.getModel()!.getLanguageId() }; } - }, 'terminal'); + }, 'terminal')); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { From 33cd6f1001b92a912898996be69b6928eda1a682 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 8 Mar 2024 16:21:04 -0800 Subject: [PATCH 099/141] Experiment in-mem cache for cell generation prompts (#207213) --- .../controller/chat/cellChatActions.ts | 47 ++++++- .../controller/chat/notebookChatController.ts | 123 +++++++++++++++--- .../browser/view/cellParts/cellContextKeys.ts | 27 +++- .../notebook/common/notebookContextKeys.ts | 1 + 4 files changed, 176 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 329ff39bf74..e14960efc0a 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -16,10 +16,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; -import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; registerAction2(class extends NotebookAction { @@ -613,3 +613,46 @@ registerAction2(class extends NotebookAction { NotebookChatController.get(context.notebookEditor)?.populateHistory(false); } }); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.restore', + title: localize2('notebookActions.restoreCellprompt', "Generate"), + icon: Codicon.sparkle, + menu: { + id: MenuId.NotebookCellTitle, + group: CELL_TITLE_CELL_GROUP_ID, + order: 0, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + NOTEBOOK_CELL_GENERATED_BY_CHAT, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const cell = context.cell; + + if (!cell) { + return; + } + + const notebookEditor = context.notebookEditor; + const controller = NotebookChatController.get(notebookEditor); + + if (!controller) { + return; + } + + const prompt = controller.getPromptFromCache(cell); + + if (prompt) { + controller.restore(cell, prompt); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 626f496569f..341593052f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -6,13 +6,15 @@ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; @@ -41,9 +43,8 @@ import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/in import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; -import { INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -68,7 +69,7 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { return this.notebookViewZone.heightInPx; } - private _editingCell: CellViewModel | null = null; + private _editingCell: ICellViewModel | null = null; get editingCell() { return this._editingCell; @@ -97,6 +98,10 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { this._layoutWidget(inlineChatWidget, widgetContainer); } + restoreEditingCell(initEditingCell: ICellViewModel) { + this._editingCell = initEditingCell; + } + hasFocus() { return this.inlineChatWidget.hasFocus(); } @@ -119,7 +124,7 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { return this._editingCell; } - async getOrCreateEditingCell(): Promise<{ cell: CellViewModel; editor: IActiveCodeEditor } | undefined> { + async getOrCreateEditingCell(): Promise<{ cell: ICellViewModel; editor: IActiveCodeEditor } | undefined> { if (this._editingCell) { const codeEditor = this._notebookEditor.codeEditors.find(ce => ce[0] === this._editingCell)?.[1]; if (codeEditor?.hasModel()) { @@ -189,6 +194,20 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { } } +export interface INotebookCellTextModelLike { uri: URI; viewType: string } +class NotebookCellTextModelLikeId { + static str(k: INotebookCellTextModelLike): string { + return `${k.viewType}/${k.uri.toString()}`; + } + static obj(s: string): INotebookCellTextModelLike { + const idx = s.indexOf('/'); + return { + viewType: s.substring(0, idx), + uri: URI.parse(s.substring(idx + 1)) + }; + } +} + export class NotebookChatController extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.chatController'; static counter: number = 0; @@ -203,7 +222,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito private _historyOffset: number = -1; private _historyCandidate: string = ''; private _historyUpdate: (prompt: string) => void; - + private _promptCache = new LRUCache(1000, 0.7); + private readonly _onDidChangePromptCache = this._register(new Emitter<{ cell: URI }>()); + readonly onDidChangePromptCache = this._onDidChangePromptCache.event; private _strategy: EditStrategy | undefined; private _sessionCtor: CancelablePromise | undefined; @@ -277,31 +298,60 @@ export class NotebookChatController extends Disposable implements INotebookEdito run(index: number, input: string | undefined, autoSend: boolean | undefined): void { if (this._widget) { - if (this._widget.afterModelPosition === index) { - // this._chatZone - // chatZone focus - } else { + if (this._widget.afterModelPosition !== index) { + this._disposeWidget(); const window = getWindow(this._widget.domNode); - this._widget.dispose(); - this._widget = undefined; - this._widgetDisposableStore.clear(); - - this._historyOffset = -1; - this._historyCandidate = ''; scheduleAtNextAnimationFrame(window, () => { - this._createWidget(index, input, autoSend); + this._createWidget(index, input, autoSend, undefined); }); } return; } - this._createWidget(index, input, autoSend); + this._createWidget(index, input, autoSend, undefined); // TODO: reveal widget to the center if it's out of the viewport } - private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) { + restore(editingCell: ICellViewModel, input: string) { + if (!this._notebookEditor.hasModel()) { + return; + } + + const index = this._notebookEditor.textModel.cells.indexOf(editingCell.model); + + if (index < 0) { + return; + } + + if (this._widget) { + if (this._widget.afterModelPosition !== index) { + this._disposeWidget(); + const window = getWindow(this._widget.domNode); + + scheduleAtNextAnimationFrame(window, () => { + this._createWidget(index, input, false, editingCell); + }); + } + + return; + } + + this._createWidget(index, input, false, editingCell); + } + + private _disposeWidget() { + this._widget?.dispose(); + this._widget = undefined; + this._widgetDisposableStore.clear(); + + this._historyOffset = -1; + this._historyCandidate = ''; + } + + + private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined, initEditingCell: ICellViewModel | undefined) { if (!this._notebookEditor.hasModel()) { return; } @@ -376,6 +426,11 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._languageService ); + if (initEditingCell) { + this._widget.restoreEditingCell(initEditingCell); + this._updateUserEditingState(); + } + this._ctxCellWidgetFocused.set(true); disposableTimeout(() => { @@ -744,6 +799,15 @@ export class NotebookChatController extends Disposable implements INotebookEdito return; } + const editingCell = this._widget?.getEditingCell(); + + if (editingCell && this._notebookEditor.hasModel() && this._activeSession.lastInput) { + const cellId = NotebookCellTextModelLikeId.str({ uri: editingCell.uri, viewType: this._notebookEditor.textModel.viewType }); + const prompt = this._activeSession.lastInput.value; + this._promptCache.set(cellId, prompt); + this._onDidChangePromptCache.fire({ cell: editingCell.uri }); + } + try { await this._strategy.apply(editor); this._inlineChatSessionService.releaseSession(this._activeSession); @@ -902,6 +966,27 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._widgetDisposableStore.clear(); } + // check if a cell is generated by prompt by checking prompt cache + isCellGeneratedByChat(cell: ICellViewModel) { + if (!this._notebookEditor.hasModel()) { + // no model attached yet + return false; + } + + const cellId = NotebookCellTextModelLikeId.str({ uri: cell.uri, viewType: this._notebookEditor.textModel.viewType }); + return this._promptCache.has(cellId); + } + + // get prompt from cache + getPromptFromCache(cell: ICellViewModel) { + if (!this._notebookEditor.hasModel()) { + // no model attached yet + return undefined; + } + + const cellId = NotebookCellTextModelLikeId.str({ uri: cell.uri, viewType: this._notebookEditor.textModel.viewType }); + return this._promptCache.get(cellId); + } public override dispose(): void { this.dismiss(false); super.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index eb2c45f2f8a..7bc1ba28b3a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -6,13 +6,14 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export class CellContextKeyPart extends CellContentPart { @@ -45,6 +46,7 @@ export class CellContextKeyManager extends Disposable { private cellOutputCollapsed!: IContextKey; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; private cellResource!: IContextKey; + private cellGeneratedByChat!: IContextKey; private markdownEditMode!: IContextKey; @@ -70,6 +72,7 @@ export class CellContextKeyManager extends Disposable { this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); + this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService); this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); if (element) { @@ -112,10 +115,21 @@ export class CellContextKeyManager extends Disposable { this.updateForEditState(); this.updateForCollapseState(); this.updateForOutputs(); + this.updateForChat(); this.cellLineNumbers.set(this.element!.lineNumbers); this.cellResource.set(this.element!.uri.toString()); }); + + const chatController = NotebookChatController.get(this.notebookEditor); + + if (chatController) { + this.elementDisposables.add(chatController.onDidChangePromptCache(e => { + if (e.cell.toString() === this.element!.uri.toString()) { + this.updateForChat(); + } + })); + } } private onDidChangeState(e: CellViewModelStateChangeEvent) { @@ -216,4 +230,15 @@ export class CellContextKeyManager extends Disposable { this.cellHasOutputs.set(false); } } + + private updateForChat() { + const chatController = NotebookChatController.get(this.notebookEditor); + + if (!chatController || !this.element) { + this.cellGeneratedByChat.set(false); + return; + } + + this.cellGeneratedByChat.set(chatController.isCellGeneratedByChat(this.element)); + } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 42b5d294c13..8345a520e5c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -46,6 +46,7 @@ export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCel export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); +export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey('notebookCellGenerateByChat', false); // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); From 50dbfdf6cd675834200198d8c96bcbe8c24778f6 Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Fri, 8 Mar 2024 20:08:20 -0500 Subject: [PATCH 100/141] Override CSS content for image icons --- src/vs/workbench/contrib/terminal/browser/terminalService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f78008e14db..de9d2dc3560 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1253,7 +1253,7 @@ class TerminalEditorStyle extends Themable { if (uri instanceof URI && iconClasses && iconClasses.length > 1) { css += ( `.monaco-workbench .terminal-tab.${iconClasses[0]}::before` + - `{background-image: ${dom.asCSSUrl(uri)};}` + `{content: ''; background-image: ${dom.asCSSUrl(uri)};}` ); } if (ThemeIcon.isThemeIcon(icon)) { From 5486af8f384ac1fff9b349469a08e1f3c38aef03 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 9 Mar 2024 10:08:42 +0100 Subject: [PATCH 101/141] editors - improve swap diff editor sides enablement (#207225) --- src/vs/workbench/browser/contextkeys.ts | 12 +++++++----- .../browser/parts/editor/diffEditorCommands.ts | 5 +++-- .../browser/parts/editor/editor.contribution.ts | 4 ++-- src/vs/workbench/common/contextkeys.ts | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index a1663743d76..71ec7e27232 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, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorOriginalWriteableContext } from 'vs/workbench/common/contextkeys'; +import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorCanSwapContext } 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, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -29,6 +29,7 @@ import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; +import { Schemas } from 'vs/base/common/network'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; @@ -41,7 +42,7 @@ export class WorkbenchContextKeysHandler extends Disposable { private activeEditorAvailableEditorIds: IContextKey; private activeEditorIsReadonly: IContextKey; - private activeCompareEditorOriginalWritable: IContextKey; + private activeCompareEditorCanSwap: IContextKey; private activeEditorCanToggleReadonly: IContextKey; private activeEditorGroupEmpty: IContextKey; @@ -130,7 +131,7 @@ export class WorkbenchContextKeysHandler extends Disposable { // Editors this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); - this.activeCompareEditorOriginalWritable = ActiveCompareEditorOriginalWriteableContext.bindTo(this.contextKeyService); + this.activeCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.contextKeyService); this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService); this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService); this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService); @@ -318,13 +319,14 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup)); applyAvailableEditorIds(this.activeEditorAvailableEditorIds, activeEditorPane.input, this.editorResolverService); this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); - this.activeCompareEditorOriginalWritable.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly()); const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); + this.activeCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); } else { this.activeEditorContext.reset(); this.activeEditorIsReadonly.reset(); - this.activeCompareEditorOriginalWritable.reset(); + this.activeCompareEditorCanSwap.reset(); this.activeEditorCanToggleReadonly.reset(); this.activeEditorCanRevert.reset(); this.activeEditorCanSplitInGroup.reset(); diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index 550ed81628f..7a782b9ee3e 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -7,10 +7,11 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { localize2, localize } from 'vs/nls'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; -import { TextCompareEditorVisibleContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; +import { TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -226,6 +227,6 @@ export function registerDiffEditorCommands(): void { title: localize2('swapDiffSides', "Swap Left and Right Editor Side"), category: localize('compare', "Compare") }, - when: TextCompareEditorActiveContext + when: ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext) }); } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index a60e8e59d62..1611076ea51 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorOriginalWriteableContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -606,7 +606,7 @@ appendEditorToolItem( title: localize('swapDiffSides', "Swap Left and Right Side"), icon: Codicon.arrowSwap }, - ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorOriginalWriteableContext), + ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext), 15, undefined, undefined diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index a6464aeacd9..612f006a88c 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -52,7 +52,7 @@ export const ActiveEditorFirstInGroupContext = new RawContextKey('activ export const ActiveEditorLastInGroupContext = new RawContextKey('activeEditorIsLastInGroup', false, localize('activeEditorIsLastInGroup', "Whether the active editor is the last one in its group")); export const ActiveEditorStickyContext = new RawContextKey('activeEditorIsPinned', false, localize('activeEditorIsPinned', "Whether the active editor is pinned")); export const ActiveEditorReadonlyContext = new RawContextKey('activeEditorIsReadonly', false, localize('activeEditorIsReadonly', "Whether the active editor is read-only")); -export const ActiveCompareEditorOriginalWriteableContext = new RawContextKey('activeCompareEditorOriginalWritable', false, localize('activeCompareEditorOriginalWritable', "Whether the active compare editor has a writable original side")); +export const ActiveCompareEditorCanSwapContext = new RawContextKey('activeCompareEditorCanSwap', false, localize('activeCompareEditorCanSwap', "Whether the active compare editor can swap sides")); export const ActiveEditorCanToggleReadonlyContext = new RawContextKey('activeEditorCanToggleReadonly', true, localize('activeEditorCanToggleReadonly', "Whether the active editor can toggle between being read-only or writeable")); export const ActiveEditorCanRevertContext = new RawContextKey('activeEditorCanRevert', false, localize('activeEditorCanRevert', "Whether the active editor can revert")); export const ActiveEditorCanSplitInGroupContext = new RawContextKey('activeEditorCanSplitInGroup', true); From 0a82e795aba44c6f64e8993b499a9d8618c83fb3 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:00:03 +0100 Subject: [PATCH 102/141] Fix activity icon size and border in high contrast theme (#207261) fix #197648 --- .../auxiliarybar/media/auxiliaryBarPart.css | 17 +++++++++++++++++ .../browser/parts/media/paneCompositePart.css | 1 + .../browser/parts/sidebar/media/sidebarpart.css | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 675a6507271..21f22875e1f 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -34,6 +34,23 @@ flex: 1; } +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus { + outline: 0 !important; /* activity bar indicates focus custom */ +} + +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { + border-radius: 0px; + outline-offset: 2px; +} + +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { + position: absolute; + left: 6px; /* place icon in center */ +} + .monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 332f9489175..153ba9ef298 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -290,6 +290,7 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; + border-top-width: 2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 4721db76de4..0f2312e1f19 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -60,6 +60,23 @@ height: 16px; } +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus { + outline: 0 !important; /* activity bar indicates focus custom */ +} + +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { + border-radius: 0px; + outline-offset: 2px; +} + +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { + position: absolute; + left: 6px; /* place icon in center */ +} + .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, From 12fc36731bd0424dfc53e9f456ac793c1eb1d6d0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:00:38 +0100 Subject: [PATCH 103/141] Increase activity bar space at top and bottom (#207262) Give more space to activity bar when top / bottom --- src/vs/workbench/browser/media/part.css | 3 +-- src/vs/workbench/browser/parts/media/paneCompositePart.css | 2 ++ src/vs/workbench/browser/parts/paneCompositePart.ts | 5 +++-- src/vs/workbench/browser/parts/panel/panelPart.ts | 4 ++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index f2787c2bf3b..f17465a5e4a 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -35,8 +35,7 @@ overflow: hidden; } -.monaco-workbench .part > .title, -.monaco-workbench .part > .header-or-footer { +.monaco-workbench .part > .title { padding-left: 8px; padding-right: 8px; } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 153ba9ef298..bc9ad38c3b8 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -21,6 +21,8 @@ } .monaco-workbench .pane-composite-part > .header-or-footer { + padding-left: 4px; + padding-right: 4px; background-color: var(--vscode-activityBarTop-background); } diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index d914587e771..634760a8213 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -545,7 +545,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart Date: Mon, 11 Mar 2024 00:27:12 -0700 Subject: [PATCH 104/141] Feature - Surface video tutorials on Welcome Page (#207293) * Initial support for video tutorials * Experiment for surfacing video tutorials --- .../browser/gettingStarted.ts | 138 ++++++++++++++++-- 1 file changed, 122 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 725248d0031..1695dfa8ce5 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -70,6 +70,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { GettingStartedIndexList } from './gettingStartedList'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -148,6 +149,7 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; + private videoList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -160,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private detailsRenderer: GettingStartedDetailsRenderer; private categoriesSlideDisposables: DisposableStore; + private showFeaturedWalkthrough = true; constructor( group: IEditorGroup, @@ -185,7 +188,9 @@ export class GettingStartedPage extends EditorPane { @IHostService private readonly hostService: IHostService, @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService) { + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService + ) { super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); @@ -345,7 +350,13 @@ export class GettingStartedPage extends EditorPane { this.dispatchListeners.clear(); this.container.querySelectorAll('[x-dispatch]').forEach(element => { - const [command, argument] = (element.getAttribute('x-dispatch') ?? '').split(':'); + const dispatch = element.getAttribute('x-dispatch') ?? ''; + let command, argument; + if (dispatch.startsWith('openLink:https')) { + [command, argument] = ['openLink', dispatch.replace('openLink:', '')]; + } else { + [command, argument] = dispatch.split(':'); + } if (command) { this.dispatchListeners.add(addDisposableListener(element, 'click', (e) => { e.stopPropagation(); @@ -433,12 +444,12 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'openExtensionPage': { - this.commandService.executeCommand('extension.open', argument); + case 'hideVideos': { + this.hideVideos(); break; } - case 'hideExtension': { - this.hideExtension(argument); + case 'openLink': { + this.openerService.open(argument); break; } default: { @@ -455,9 +466,9 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideExtension(extensionId: string) { - this.setHiddenCategories([...this.getHiddenCategories().add(extensionId)]); - this.registerDispatchListeners(); + private hideVideos() { + this.setHiddenCategories([...this.getHiddenCategories().add('getting-started-videos')]); + this.videoList?.setEntries(undefined); } private markAllStepsComplete() { @@ -807,6 +818,29 @@ export class GettingStartedPage extends EditorPane { const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); + + const showVideoTutorials = await Promise.race([ + this.tasExperimentService?.getTreatment('gettingStarted.showVideoTutorials'), + new Promise(resolve => setTimeout(() => resolve(false), 200)) + ]); + + let videoList: GettingStartedIndexList; + if (showVideoTutorials === true) { + this.showFeaturedWalkthrough = false; + videoList = this.buildVideosList(); + const layoutVideos = () => { + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); + } + else { + reset(rightColumn, gettingStartedList.getDomElement()); + } + setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); + layoutRecentList(); + }; + videoList.onDidChange(layoutVideos); + } + const gettingStartedList = this.buildGettingStartedWalkthroughsList(); const footer = $('.footer', {}, @@ -818,19 +852,27 @@ export class GettingStartedPage extends EditorPane { const layoutLists = () => { if (gettingStartedList.itemCount) { this.container.classList.remove('noWalkthroughs'); - reset(rightColumn, gettingStartedList.getDomElement()); + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); + } else { + reset(rightColumn, gettingStartedList.getDomElement()); + } } else { this.container.classList.add('noWalkthroughs'); - reset(rightColumn); - + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement()); + } + else { + reset(rightColumn); + } } setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); layoutRecentList(); }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs')) { + if (this.container.classList.contains('noWalkthroughs') && videoList?.itemCount === 0) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); @@ -873,7 +915,7 @@ export class GettingStartedPage extends EditorPane { const telemetryNotice = $('p.telemetry-notice'); this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); - } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { + } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory && this.showFeaturedWalkthrough) { const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; @@ -1018,7 +1060,7 @@ export class GettingStartedPage extends EditorPane { const featuredBadge = $('.featured-badge', {}); const descriptionContent = $('.description-content', {},); - if (category.isFeatured) { + if (category.isFeatured && this.showFeaturedWalkthrough) { reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); reset(descriptionContent, ...renderLabelWithIcons(category.description)); } @@ -1026,7 +1068,7 @@ export class GettingStartedPage extends EditorPane { const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': category.id }); reset(titleContent, ...renderLabelWithIcons(category.title)); - return $('button.getting-started-category' + (category.isFeatured ? '.featured' : ''), + return $('button.getting-started-category' + (category.isFeatured && this.showFeaturedWalkthrough ? '.featured' : ''), { 'x-dispatch': 'selectCategory:' + category.id, 'title': category.description @@ -1090,6 +1132,69 @@ export class GettingStartedPage extends EditorPane { return gettingStartedList; } + private buildVideosList(): GettingStartedIndexList { + + const renderFeaturedExtensions = (entry: IWelcomePageStartEntry): HTMLElement => { + + const featuredBadge = $('.featured-badge', {}); + const descriptionContent = $('.description-content', {},); + + reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); + reset(descriptionContent, ...renderLabelWithIcons(entry.description)); + + const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': entry.id }); + reset(titleContent, ...renderLabelWithIcons(entry.title)); + + return $('button.getting-started-category' + '.featured', + { + 'x-dispatch': 'openLink:' + entry.command, + 'title': entry.title + }, + featuredBadge, + $('.main-content', {}, + this.iconWidgetFor(entry), + titleContent, + $('a.codicon.codicon-close.hide-category-button', { + 'tabindex': 0, + 'x-dispatch': 'hideVideos', + 'title': localize('close', "Hide"), + 'role': 'button', + 'aria-label': localize('closeAriaLabel', "Hide"), + }), + ), + descriptionContent); + }; + + if (this.videoList) { + this.videoList.dispose(); + } + const videoList = this.videoList = new GettingStartedIndexList( + { + title: '', + klass: 'getting-started-videos', + limit: 1, + renderElement: renderFeaturedExtensions, + contextService: this.contextService, + }); + + if (this.getHiddenCategories().has('getting-started-videos')) { + return videoList; + } + + videoList.setEntries([{ + id: 'getting-started-videos', + title: localize('videos-title', 'Discover Getting Started Tutorials'), + description: localize('videos-description', 'Learn VS Code\'s must-have features in short and practical videos'), + command: 'https://aka.ms/vscode-getting-started-tutorials', + order: 0, + icon: { type: 'icon', icon: Codicon.play }, + when: ContextKeyExpr.true(), + }]); + videoList.onDidChange(() => this.registerDispatchListeners()); + + return videoList; + } + layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1099,6 +1204,7 @@ export class GettingStartedPage extends EditorPane { this.startList?.layout(size); this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); + this.videoList?.layout(size); if (this.editorInput?.selectedStep && this.currentMediaType) { this.mediaDisposables.clear(); From 8c8105002c089e657402b0f7ced80b2ecae6870c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 11 Mar 2024 10:05:24 +0100 Subject: [PATCH 105/141] Revert "Refactors: Reduces assumptions about line height." This reverts commit a9ab31da9cfa2d45bac4bda59a980cc701d497e5. --- src/vs/editor/browser/view/viewLayer.ts | 10 +++--- src/vs/editor/browser/view/viewOverlays.ts | 30 +++++++++++----- .../currentLineHighlight.css | 6 +--- .../currentLineHighlight.ts | 7 ++-- .../viewParts/decorations/decorations.css | 3 +- .../viewParts/decorations/decorations.ts | 23 ++++++++----- .../viewParts/indentGuides/indentGuides.css | 1 - .../viewParts/indentGuides/indentGuides.ts | 6 +++- .../viewParts/lineNumbers/lineNumbers.css | 2 +- .../browser/viewParts/lines/viewLine.ts | 8 ++--- .../browser/viewParts/lines/viewLines.css | 5 --- .../viewParts/selections/selections.ts | 34 ++++++++++++------- .../viewParts/whitespace/whitespace.ts | 2 +- .../browser/widget/codeEditor/editor.css | 9 ----- .../editor/common/viewLayout/linesLayout.ts | 3 +- .../viewLayout/viewLinesViewportData.ts | 3 -- src/vs/editor/common/viewModel.ts | 5 --- 17 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index bbbb0dd9d73..c15239ec8b1 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -22,12 +22,12 @@ export interface IVisibleLine extends ILine { * Return null if the HTML should not be touched. * Return the new HTML otherwise. */ - renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean; + renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean; /** * Layout the line. */ - layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void; + layoutLine(lineNumber: number, deltaTop: number): void; } export interface ILine { @@ -465,7 +465,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this.viewportData.lineHeight); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN]); } } @@ -573,7 +573,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -603,7 +603,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 1041fd58a58..83a3cc05d6f 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -9,6 +9,7 @@ import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { IVisibleLine, IVisibleLinesHost, VisibleLinesCollection } from 'vs/editor/browser/view/viewLayer'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -70,7 +71,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost | null; private _renderedContent: string | null; + private _lineHeight: number; - constructor(dynamicOverlays: DynamicViewOverlay[]) { + constructor(configuration: IEditorConfiguration, dynamicOverlays: DynamicViewOverlay[]) { + this._configuration = configuration; + this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); this._dynamicOverlays = dynamicOverlays; this._domNode = null; @@ -169,8 +180,11 @@ export class ViewOverlayLine implements IVisibleLine { public onTokensChanged(): void { // Nothing } + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { + this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); + } - public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { let result = ''; for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) { const dynamicOverlay = this._dynamicOverlays[i]; @@ -184,10 +198,10 @@ export class ViewOverlayLine implements IVisibleLine { this._renderedContent = result; - sb.appendString('
'); sb.appendString(result); sb.appendString('
'); @@ -195,10 +209,10 @@ export class ViewOverlayLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { + public layoutLine(lineNumber: number, deltaTop: number): void { if (this._domNode) { this._domNode.setTop(deltaTop); - this._domNode.setHeight(lineHeight); + this._domNode.setHeight(this._lineHeight); } } } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css index 403e255fac8..2a0e39dffa7 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css @@ -9,7 +9,6 @@ left: 0; top: 0; box-sizing: border-box; - height: 100%; } .monaco-editor .margin-view-overlays .current-line { @@ -18,11 +17,8 @@ left: 0; top: 0; box-sizing: border-box; - height: 100%; } -.monaco-editor - .margin-view-overlays - .current-line.current-line-margin.current-line-margin-both { +.monaco-editor .margin-view-overlays .current-line.current-line-margin.current-line-margin-both { border-right: 0; } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index b35970ee373..64649e0b835 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -18,6 +18,7 @@ import { Position } from 'vs/editor/common/core/position'; export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; + protected _lineHeight: number; protected _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; protected _wordWrap: boolean; protected _contentLeft: number; @@ -38,6 +39,7 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); + this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -87,6 +89,7 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); + this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -205,7 +208,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-both' : '') + (exact ? ' current-line-exact' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return this._shouldRenderInContent(); @@ -218,7 +221,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return true; diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.css b/src/vs/editor/browser/viewParts/decorations/decorations.css index 4c755e2dbf8..37c39f620e8 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/src/vs/editor/browser/viewParts/decorations/decorations.css @@ -9,5 +9,4 @@ */ .monaco-editor .lines-content .cdr { position: absolute; - height: 100%; -} +} \ No newline at end of file diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index a3baa510464..fe495466b1d 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -15,6 +15,7 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; export class DecorationsOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; + private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _renderResult: string[] | null; @@ -22,6 +23,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; + this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._renderResult = null; @@ -38,6 +40,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; + this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; } @@ -113,6 +116,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { + const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; @@ -126,7 +130,9 @@ export class DecorationsOverlay extends DynamicViewOverlay { const decorationOutput = ( '
' + + '" style="left:0;width:100%;height:' + + lineHeight + + 'px;">' ); const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); @@ -139,6 +145,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { + const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; let prevClassName: string | null = null; @@ -169,7 +176,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { // flush previous decoration if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); } prevClassName = className; @@ -179,11 +186,11 @@ export class DecorationsOverlay extends DynamicViewOverlay { } if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); } } - private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, visibleStartLineNumber: number, output: string[]): void { + private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, lineHeight: string, visibleStartLineNumber: number, output: string[]): void { const linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); if (!linesVisibleRanges) { return; @@ -215,12 +222,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { + className + '" style="left:' + String(visibleRange.left) - + 'px;width:' + (expandToLeft ? - '100%;' : - (String(visibleRange.width) + 'px;') + 'px;width:100%;height:' : + ('px;width:' + String(visibleRange.width) + 'px;height:') ) - + '">' + + lineHeight + + 'px;">' ); output[lineIndex] += decorationOutput; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index 6aacf7c2126..ed132669757 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -6,5 +6,4 @@ .monaco-editor .lines-content .core-guide { position: absolute; box-sizing: border-box; - height: 100%; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 50b0b2b8661..a93cf75a530 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -22,6 +22,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; private _primaryPosition: Position | null; + private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[] | null; private _maxIndentLeft: number; @@ -36,6 +37,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); + this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -58,6 +60,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); + this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -111,6 +114,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const scrollWidth = ctx.scrollWidth; + const lineHeight = this._lineHeight; const activeCursorPosition = this._primaryPosition; @@ -146,7 +150,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { )?.left ?? (left + this._spaceWidth)) - left : this._spaceWidth; - result += `
`; + result += `
`; } output[lineIndex] = result; } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 2961137b032..774ffef273d 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .margin-view-overlays .line-numbers { - bottom: 0; font-variant-numeric: tabular-nums; position: absolute; text-align: right; @@ -12,6 +11,7 @@ vertical-align: middle; box-sizing: border-box; cursor: default; + height: 100%; } .monaco-editor .relative-current-line-number { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 9a5d2f556bf..e4174a2f286 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -151,7 +151,7 @@ export class ViewLine implements IVisibleLine { return false; } - public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; @@ -222,7 +222,7 @@ export class ViewLine implements IVisibleLine { sb.appendString('
'); @@ -255,10 +255,10 @@ export class ViewLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { + public layoutLine(lineNumber: number, deltaTop: number): void { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); - this._renderedViewLine.domNode.setHeight(lineHeight); + this._renderedViewLine.domNode.setHeight(this._options.lineHeight); } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 5bb02a5338c..fe686d3e441 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,11 +63,6 @@ width: 100%; } -.monaco-editor .view-line > span { - bottom: 0; - position: absolute; -} - .monaco-editor .mtkw { color: var(--vscode-editorWhitespace-foreground) !important; } diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index d53a5126e62..efceef0e5c3 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -68,6 +68,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { private static readonly ROUNDED_PIECE_WIDTH = 10; private readonly _context: ViewContext; + private _lineHeight: number; private _roundedSelection: boolean; private _typicalHalfwidthCharacterWidth: number; private _selections: Range[]; @@ -77,6 +78,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; + this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._selections = []; @@ -94,6 +96,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; + this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; @@ -252,16 +255,19 @@ export class SelectionsOverlay extends DynamicViewOverlay { return linesVisibleRanges; } - private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string { + private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string { return ( '
' + + '" style="top:' + + top.toString() + + 'px;left:' + + left.toString() + + 'px;width:' + + width.toString() + + 'px;height:' + + height + + 'px;">
' ); } @@ -271,6 +277,8 @@ export class SelectionsOverlay extends DynamicViewOverlay { } const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle; + const fullLineHeight = (this._lineHeight).toString(); + const reducedLineHeight = (this._lineHeight - 1).toString(); const firstLineNumber = visibleRanges[0].lineNumber; const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber; @@ -280,8 +288,8 @@ export class SelectionsOverlay extends DynamicViewOverlay { const lineNumber = lineVisibleRanges.lineNumber; const lineIndex = lineNumber - visibleStartLineNumber; + const lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight; const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0; - const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0; let innerCornerOutput = ''; let restOfSelectionOutput = ''; @@ -296,7 +304,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { // Reverse rounded corner to the left // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -306,13 +314,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (startStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } - innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) { // Reverse rounded corner to the right // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -322,7 +330,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (endStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT; } - innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } } @@ -343,7 +351,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } } - restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width); + restOfSelectionOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width); } output2[lineIndex][0] += innerCornerOutput; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 3bd29fc5e1e..489293a01b8 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -235,7 +235,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { if (USE_SVG) { maxLeft = Math.round(maxLeft + spaceWidth); return ( - `` + `` + result + `` ); diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 09c4a32f141..1d60940158a 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -56,15 +56,6 @@ top: 0; } -.monaco-editor .view-overlays > div, .monaco-editor .margin-view-overlays > div { - position: absolute; - width: 100%; -} - -.monaco-editor .view-overlays > div > div, .monaco-editor .margin-view-overlays > div > div { - bottom: 0; -} - /* .monaco-editor .auto-closed-character { opacity: 0.3; diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 71bf9d5b956..7bb55aeef6e 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -727,8 +727,7 @@ export class LinesLayout { relativeVerticalOffset: linesOffsets, centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, - completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, - lineHeight: this._lineHeight, + completelyVisibleEndLineNumber: completelyVisibleEndLineNumber }; } diff --git a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 6e072c52648..8ddcfddb99d 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -46,8 +46,6 @@ export class ViewportData { private readonly _model: IViewModel; - public readonly lineHeight: number; - constructor( selections: Selection[], partialData: IPartialViewLinesViewportData, @@ -59,7 +57,6 @@ export class ViewportData { this.endLineNumber = partialData.endLineNumber | 0; this.relativeVerticalOffset = partialData.relativeVerticalOffset; this.bigNumbersDelta = partialData.bigNumbersDelta | 0; - this.lineHeight = partialData.lineHeight | 0; this.whitespaceViewportData = whitespaceViewportData; this._model = model; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 29a01bcf904..4f92417e89b 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -181,11 +181,6 @@ export interface IPartialViewLinesViewportData { * The last completely visible line number. */ readonly completelyVisibleEndLineNumber: number; - - /** - * The height of a line. - */ - readonly lineHeight: number; } export interface IViewWhitespaceViewportData { From e4f8e9fa54826aa92ec5b4a284e660c45dd1663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20=C3=85sberg?= Date: Mon, 11 Mar 2024 11:43:05 +0100 Subject: [PATCH 106/141] Render final line number for interval setting (#207227) --- src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 03336d34278..dcebb27994e 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -131,6 +131,10 @@ export class LineNumbersOverlay extends DynamicViewOverlay { if (modelLineNumber % 10 === 0) { return String(modelLineNumber); } + const finalLineNumber = this._context.viewModel.getLineCount(); + if (modelLineNumber === finalLineNumber) { + return String(modelLineNumber); + } return ''; } From 989dc339df946fc377ef73167b20c2fcb7eadf20 Mon Sep 17 00:00:00 2001 From: Brook Mao Date: Mon, 11 Mar 2024 06:47:58 -0400 Subject: [PATCH 107/141] Improved description for editor.useTabStops setting (#206552) * Update description for editor.useTabStops setting --- src/vs/editor/common/config/editorOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index bd51c9ec27d..9eaa2858b05 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5987,7 +5987,7 @@ export const EditorOptions = { )), useTabStops: register(new EditorBooleanOption( EditorOption.useTabStops, 'useTabStops', true, - { description: nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops.") } + { description: nls.localize('useTabStops', "Spaces and tabs are inserted and deleted in alignment with tab stops.") } )), wordBreak: register(new EditorStringEnumOption( EditorOption.wordBreak, 'wordBreak', From 6bd027eda0f94548c656acd2daeeeb90b3cc02a8 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Mon, 11 Mar 2024 15:48:46 +0200 Subject: [PATCH 108/141] Tunnel: Re-add unit tests for port mapping (#207249) This reverts commit b79e884dc16ef639963192597fab46294608217a and implements it with proper mocking. --- src/vs/workbench/browser/window.ts | 2 +- src/vs/workbench/electron-sandbox/window.ts | 4 +- .../electron-sandbox/resolveExternal.test.ts | 129 ++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 121f3528fe4..e964af6543e 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -101,7 +101,7 @@ export abstract class BaseWindow extends Disposable { //#region timeout handling in multi-window applications - private enableMultiWindowAwareTimeout(targetWindow: Window, dom = { getWindowsCount, getWindows }): void { + protected enableMultiWindowAwareTimeout(targetWindow: Window, dom = { getWindowsCount, getWindows }): void { // Override `setTimeout` and `clearTimeout` on the provided window to make // sure timeouts are dispatched to all opened windows. Some browsers may decide diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 9f9877a34ff..257e85e7902 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -139,7 +139,7 @@ export class NativeWindow extends BaseWindow { this.create(); } - private registerListeners(): void { + protected registerListeners(): void { // Layout this._register(addDisposableListener(mainWindow, EventType.RESIZE, () => this.layoutService.layout())); @@ -644,7 +644,7 @@ export class NativeWindow extends BaseWindow { } } - private create(): void { + protected create(): void { // Handle open calls this.setupOpenHandlers(); diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts new file mode 100644 index 00000000000..5e959538012 --- /dev/null +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; +import { URI } from 'vs/base/common/uri'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { workbenchInstantiationService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +type PortMap = Record; + +class TunnelMock implements Partial { + private assignedPorts: PortMap = {}; + private expectedDispose = false; + + reset(ports: PortMap) { + this.assignedPorts = ports; + } + + expectDispose() { + this.expectedDispose = true; + } + + getExistingTunnel(): Promise { + return Promise.resolve(undefined); + } + + openTunnel(_addressProvider: IAddressProvider | undefined, _host: string | undefined, port: number): Promise | undefined { + if (!this.assignedPorts[port]) { + return Promise.reject(new Error('Unexpected tunnel request')); + } + const res: RemoteTunnel = { + localAddress: `localhost:${this.assignedPorts[port]}`, + tunnelRemoteHost: '4.3.2.1', + tunnelRemotePort: this.assignedPorts[port], + privacy: '', + dispose: () => { + assert(this.expectedDispose, 'Unexpected dispose'); + this.expectedDispose = false; + return Promise.resolve(); + } + }; + delete this.assignedPorts[port]; + return Promise.resolve(res); + } + + validate() { + try { + assert(Object.keys(this.assignedPorts).length === 0, 'Expected tunnel to be used'); + assert(!this.expectedDispose, 'Expected dispose to be called'); + } finally { + this.expectedDispose = false; + } + } +} + +class TestNativeWindow extends NativeWindow { + protected override create(): void { } + protected override registerListeners(): void { } + protected override enableMultiWindowAwareTimeout(): void { } +} + +suite('NativeWindow:resolveExternal', () => { + const disposables = new DisposableStore(); + const tunnelMock = new TunnelMock(); + let window: TestNativeWindow; + + suiteSetup(() => { + const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); + instantiationService.stub(ITunnelService, tunnelMock); + window = disposables.add(instantiationService.createInstance(TestNativeWindow)); + }); + + suiteTeardown(() => { + disposables.clear(); + }); + + async function doTest(uri: string, ports: PortMap = {}, expectedUri?: string) { + tunnelMock.reset(ports); + const res = await window.resolveExternalUri(URI.parse(uri), { + allowTunneling: true, + openExternal: true + }); + assert.strictEqual(!expectedUri, !res, `Expected URI ${expectedUri} but got ${res}`); + if (expectedUri && res) { + assert.strictEqual(res.resolved.toString(), URI.parse(expectedUri).toString()); + } + tunnelMock.validate(); + } + + test('invalid', async () => { + await doTest('file:///foo.bar/baz'); + await doTest('http://foo.bar/path'); + }); + test('simple', async () => { + await doTest('http://localhost:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); + }); + test('all interfaces', async () => { + await doTest('http://0.0.0.0:1234/path', { 1234: 1234 }, 'http://localhost:1234/path'); + }); + test('changed port', async () => { + await doTest('http://localhost:1234/path', { 1234: 1235 }, 'http://localhost:1235/path'); + }); + test('query', async () => { + await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4455 }, 'http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); + test('query with different port', async () => { + tunnelMock.expectDispose(); + await doTest('http://foo.bar/path?a=b&c=http%3a%2f%2flocalhost%3a4455', { 4455: 4567 }); + }); + test('both url and query', async () => { + await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', + { 1234: 4321, 4455: 4455 }, + 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); + test('both url and query, query rejected', async () => { + tunnelMock.expectDispose(); + await doTest('http://localhost:1234/path?a=b&c=http%3a%2f%2flocalhost%3a4455', + { 1234: 4321, 4455: 5544 }, + 'http://localhost:4321/path?a=b&c=http%3a%2f%2flocalhost%3a4455'); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); From 5faa55a173571edace9b667b9f4dd5e3b5136de3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 11 Mar 2024 12:47:38 +0100 Subject: [PATCH 109/141] Reapply "Refactors: Reduces assumptions about line height." This reverts commit 8c8105002c089e657402b0f7ced80b2ecae6870c. --- src/vs/editor/browser/view/viewLayer.ts | 10 +++--- src/vs/editor/browser/view/viewOverlays.ts | 30 +++++----------- .../currentLineHighlight.css | 6 +++- .../currentLineHighlight.ts | 7 ++-- .../viewParts/decorations/decorations.css | 3 +- .../viewParts/decorations/decorations.ts | 23 +++++-------- .../viewParts/indentGuides/indentGuides.css | 1 + .../viewParts/indentGuides/indentGuides.ts | 6 +--- .../viewParts/lineNumbers/lineNumbers.css | 2 +- .../browser/viewParts/lines/viewLine.ts | 8 ++--- .../browser/viewParts/lines/viewLines.css | 5 +++ .../viewParts/selections/selections.ts | 34 +++++++------------ .../viewParts/whitespace/whitespace.ts | 2 +- .../browser/widget/codeEditor/editor.css | 9 +++++ .../editor/common/viewLayout/linesLayout.ts | 3 +- .../viewLayout/viewLinesViewportData.ts | 3 ++ src/vs/editor/common/viewModel.ts | 5 +++ 17 files changed, 75 insertions(+), 82 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index c15239ec8b1..bbbb0dd9d73 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -22,12 +22,12 @@ export interface IVisibleLine extends ILine { * Return null if the HTML should not be touched. * Return the new HTML otherwise. */ - renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean; + renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean; /** * Layout the line. */ - layoutLine(lineNumber: number, deltaTop: number): void; + layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void; } export interface ILine { @@ -465,7 +465,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN]); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this.viewportData.lineHeight); } } @@ -573,7 +573,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -603,7 +603,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 83a3cc05d6f..1041fd58a58 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -9,7 +9,6 @@ import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { IVisibleLine, IVisibleLinesHost, VisibleLinesCollection } from 'vs/editor/browser/view/viewLayer'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -71,7 +70,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost | null; private _renderedContent: string | null; - private _lineHeight: number; - constructor(configuration: IEditorConfiguration, dynamicOverlays: DynamicViewOverlay[]) { - this._configuration = configuration; - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); + constructor(dynamicOverlays: DynamicViewOverlay[]) { this._dynamicOverlays = dynamicOverlays; this._domNode = null; @@ -180,11 +169,8 @@ export class ViewOverlayLine implements IVisibleLine { public onTokensChanged(): void { // Nothing } - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); - } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { let result = ''; for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) { const dynamicOverlay = this._dynamicOverlays[i]; @@ -198,10 +184,10 @@ export class ViewOverlayLine implements IVisibleLine { this._renderedContent = result; - sb.appendString('
'); sb.appendString(result); sb.appendString('
'); @@ -209,10 +195,10 @@ export class ViewOverlayLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._domNode) { this._domNode.setTop(deltaTop); - this._domNode.setHeight(this._lineHeight); + this._domNode.setHeight(lineHeight); } } } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css index 2a0e39dffa7..403e255fac8 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css @@ -9,6 +9,7 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } .monaco-editor .margin-view-overlays .current-line { @@ -17,8 +18,11 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } -.monaco-editor .margin-view-overlays .current-line.current-line-margin.current-line-margin-both { +.monaco-editor + .margin-view-overlays + .current-line.current-line-margin.current-line-margin-both { border-right: 0; } diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 64649e0b835..b35970ee373 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -18,7 +18,6 @@ import { Position } from 'vs/editor/common/core/position'; export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - protected _lineHeight: number; protected _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; protected _wordWrap: boolean; protected _contentLeft: number; @@ -39,7 +38,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -89,7 +87,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -208,7 +205,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-both' : '') + (exact ? ' current-line-exact' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return this._shouldRenderInContent(); @@ -221,7 +218,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return true; diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.css b/src/vs/editor/browser/viewParts/decorations/decorations.css index 37c39f620e8..4c755e2dbf8 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/src/vs/editor/browser/viewParts/decorations/decorations.css @@ -9,4 +9,5 @@ */ .monaco-editor .lines-content .cdr { position: absolute; -} \ No newline at end of file + height: 100%; +} diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index fe495466b1d..a3baa510464 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -15,7 +15,6 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; export class DecorationsOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _renderResult: string[] | null; @@ -23,7 +22,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._renderResult = null; @@ -40,7 +38,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; } @@ -116,7 +113,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; @@ -130,9 +126,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { const decorationOutput = ( '
' + + '" style="left:0;width:100%;">' ); const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); @@ -145,7 +139,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; let prevClassName: string | null = null; @@ -176,7 +169,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { // flush previous decoration if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } prevClassName = className; @@ -186,11 +179,11 @@ export class DecorationsOverlay extends DynamicViewOverlay { } if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } } - private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, lineHeight: string, visibleStartLineNumber: number, output: string[]): void { + private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, visibleStartLineNumber: number, output: string[]): void { const linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); if (!linesVisibleRanges) { return; @@ -222,12 +215,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { + className + '" style="left:' + String(visibleRange.left) + + 'px;width:' + (expandToLeft ? - 'px;width:100%;height:' : - ('px;width:' + String(visibleRange.width) + 'px;height:') + '100%;' : + (String(visibleRange.width) + 'px;') ) - + lineHeight - + 'px;">' + + '">' ); output[lineIndex] += decorationOutput; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index ed132669757..6aacf7c2126 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -6,4 +6,5 @@ .monaco-editor .lines-content .core-guide { position: absolute; box-sizing: border-box; + height: 100%; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index a93cf75a530..50b0b2b8661 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -22,7 +22,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; private _primaryPosition: Position | null; - private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[] | null; private _maxIndentLeft: number; @@ -37,7 +36,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -60,7 +58,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -114,7 +111,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const scrollWidth = ctx.scrollWidth; - const lineHeight = this._lineHeight; const activeCursorPosition = this._primaryPosition; @@ -150,7 +146,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { )?.left ?? (left + this._spaceWidth)) - left : this._spaceWidth; - result += `
`; + result += `
`; } output[lineIndex] = result; } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 774ffef273d..2961137b032 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .margin-view-overlays .line-numbers { + bottom: 0; font-variant-numeric: tabular-nums; position: absolute; text-align: right; @@ -11,7 +12,6 @@ vertical-align: middle; box-sizing: border-box; cursor: default; - height: 100%; } .monaco-editor .relative-current-line-number { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index e4174a2f286..9a5d2f556bf 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -151,7 +151,7 @@ export class ViewLine implements IVisibleLine { return false; } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; @@ -222,7 +222,7 @@ export class ViewLine implements IVisibleLine { sb.appendString('
'); @@ -255,10 +255,10 @@ export class ViewLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); - this._renderedViewLine.domNode.setHeight(this._options.lineHeight); + this._renderedViewLine.domNode.setHeight(lineHeight); } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index fe686d3e441..5bb02a5338c 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,6 +63,11 @@ width: 100%; } +.monaco-editor .view-line > span { + bottom: 0; + position: absolute; +} + .monaco-editor .mtkw { color: var(--vscode-editorWhitespace-foreground) !important; } diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index efceef0e5c3..d53a5126e62 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -68,7 +68,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { private static readonly ROUNDED_PIECE_WIDTH = 10; private readonly _context: ViewContext; - private _lineHeight: number; private _roundedSelection: boolean; private _typicalHalfwidthCharacterWidth: number; private _selections: Range[]; @@ -78,7 +77,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._selections = []; @@ -96,7 +94,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; @@ -255,19 +252,16 @@ export class SelectionsOverlay extends DynamicViewOverlay { return linesVisibleRanges; } - private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string { + private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string { return ( '
' + + '" style="' + + 'top:' + top.toString() + 'px;' + + 'bottom:' + bottom.toString() + 'px;' + + 'left:' + left.toString() + 'px;' + + 'width:' + width.toString() + 'px;' + + '">
' ); } @@ -277,8 +271,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { } const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle; - const fullLineHeight = (this._lineHeight).toString(); - const reducedLineHeight = (this._lineHeight - 1).toString(); const firstLineNumber = visibleRanges[0].lineNumber; const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber; @@ -288,8 +280,8 @@ export class SelectionsOverlay extends DynamicViewOverlay { const lineNumber = lineVisibleRanges.lineNumber; const lineIndex = lineNumber - visibleStartLineNumber; - const lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight; const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0; + const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0; let innerCornerOutput = ''; let restOfSelectionOutput = ''; @@ -304,7 +296,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { // Reverse rounded corner to the left // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -314,13 +306,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (startStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) { // Reverse rounded corner to the right // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -330,7 +322,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (endStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } } @@ -351,7 +343,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } } - restOfSelectionOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width); + restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width); } output2[lineIndex][0] += innerCornerOutput; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 489293a01b8..3bd29fc5e1e 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -235,7 +235,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { if (USE_SVG) { maxLeft = Math.round(maxLeft + spaceWidth); return ( - `` + `` + result + `` ); diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 1d60940158a..09c4a32f141 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -56,6 +56,15 @@ top: 0; } +.monaco-editor .view-overlays > div, .monaco-editor .margin-view-overlays > div { + position: absolute; + width: 100%; +} + +.monaco-editor .view-overlays > div > div, .monaco-editor .margin-view-overlays > div > div { + bottom: 0; +} + /* .monaco-editor .auto-closed-character { opacity: 0.3; diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 7bb55aeef6e..71bf9d5b956 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -727,7 +727,8 @@ export class LinesLayout { relativeVerticalOffset: linesOffsets, centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, - completelyVisibleEndLineNumber: completelyVisibleEndLineNumber + completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, + lineHeight: this._lineHeight, }; } diff --git a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 8ddcfddb99d..6e072c52648 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -46,6 +46,8 @@ export class ViewportData { private readonly _model: IViewModel; + public readonly lineHeight: number; + constructor( selections: Selection[], partialData: IPartialViewLinesViewportData, @@ -57,6 +59,7 @@ export class ViewportData { this.endLineNumber = partialData.endLineNumber | 0; this.relativeVerticalOffset = partialData.relativeVerticalOffset; this.bigNumbersDelta = partialData.bigNumbersDelta | 0; + this.lineHeight = partialData.lineHeight | 0; this.whitespaceViewportData = whitespaceViewportData; this._model = model; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 4f92417e89b..29a01bcf904 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -181,6 +181,11 @@ export interface IPartialViewLinesViewportData { * The last completely visible line number. */ readonly completelyVisibleEndLineNumber: number; + + /** + * The height of a line. + */ + readonly lineHeight: number; } export interface IViewWhitespaceViewportData { From 97c8d7e7ccf9028bba26ea47d284bbce93e8a1e8 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 11 Mar 2024 12:48:42 +0100 Subject: [PATCH 110/141] Fixes rendering glitch. Signed-off-by: Henning Dieterichs --- src/vs/editor/browser/viewParts/lines/viewLines.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 5bb02a5338c..e4e187c2be2 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,7 +63,8 @@ width: 100%; } -.monaco-editor .view-line > span { +/* There are view-lines in view-zones. We have to make sure this rule does not apply to them, as they don't set a line height */ +.monaco-editor .lines-content > .view-lines > .view-line > span { bottom: 0; position: absolute; } From 7efbc475f15a4f12789d037a00311bdfd7b0d86f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 08:38:20 -0700 Subject: [PATCH 111/141] use service rather than registry --- .../accessibility/browser/accessibleView.ts | 7 ++++--- .../browser/actions/chatCodeblockActions.ts | 5 +++-- .../contrib/chat/browser/chat.contribution.ts | 3 ++- src/vs/workbench/contrib/chat/browser/chat.ts | 21 ++++++++++--------- .../chat/browser/terminalChatController.ts | 5 +++-- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index e51a8bebeb1..6d9dc390ab8 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -42,7 +42,7 @@ import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQui import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { CodeBlockContextProviderRegistry } from 'vs/workbench/contrib/chat/browser/chat'; +import { ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -172,7 +172,8 @@ export class AccessibleView extends Disposable { @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, @IMenuService private readonly _menuService: IMenuService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, + @ICodeBlockContextProviderService private readonly _codeBlockContextProviderService: ICodeBlockContextProviderService ) { super(); @@ -340,7 +341,7 @@ export class AccessibleView extends Disposable { this._lastProvider = provider; } if (provider.id === AccessibleViewProviderId.Chat) { - this._register(CodeBlockContextProviderRegistry.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView')); + this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView')); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 2793bf70f13..edef26447ea 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -26,7 +26,7 @@ import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { CodeBlockContextProviderRegistry, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetService, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; @@ -563,6 +563,7 @@ export function registerChatCodeBlockActions() { function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); + const codeBlockContextProviderService = accessor.get(ICodeBlockContextProviderService); const model = editor.getModel(); if (!model) { return; @@ -571,7 +572,7 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): const widget = chatWidgetService.lastFocusedWidget; const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri); if (!codeBlockInfo) { - for (const provider of CodeBlockContextProviderRegistry.providers) { + for (const provider of codeBlockContextProviderService.providers) { const context = provider.getCodeBlockContext(editor); if (context) { return context; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 000c78ced65..5e2d466ff78 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -32,7 +32,7 @@ import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/act import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CodeBlockContextProviderService, IChatAccessibilityService, IChatWidget, IChatWidgetService, ICodeBlockContextProviderService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; @@ -331,3 +331,4 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); +registerSingleton(ICodeBlockContextProviderService, CodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index ecc65df2abf..f3f4301408e 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -8,7 +8,6 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; -import { registerSingleton, InstantiationType } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -19,7 +18,7 @@ import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWel export const IChatWidgetService = createDecorator('chatWidgetService'); export const IQuickChatService = createDecorator('quickChatService'); export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); -export const ICodeBlockContextProviderRegistry = createDecorator('codeBlockContextProviderRegistry'); +export const ICodeBlockContextProviderService = createDecorator('codeBlockContextProviderService'); export interface IChatWidgetService { @@ -143,18 +142,20 @@ export interface IChatViewPane { export interface ICodeBlockActionContextProvider { getCodeBlockContext(editor?: ICodeEditor): ICodeBlockActionContext | undefined; } -export interface ICodeBlockContextProviderRegistry { - serviceBrand: undefined; +export interface ICodeBlockContextProviderService { + readonly _serviceBrand: undefined; + readonly providers: ICodeBlockActionContextProvider[]; + registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable; } -export class CodeBlockContextProviderRegistry implements ICodeBlockContextProviderRegistry { - serviceBrand: undefined; - static readonly _providers = new Map(); - static get providers(): ICodeBlockActionContextProvider[] { +export class CodeBlockContextProviderService implements ICodeBlockContextProviderService { + declare _serviceBrand: undefined; + private readonly _providers = new Map(); + + get providers(): ICodeBlockActionContextProvider[] { return [...this._providers.values()]; } - static registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { + registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { this._providers.set(id, provider); return toDisposable(() => this._providers.delete(id)); } } -registerSingleton(ICodeBlockContextProviderRegistry, CodeBlockContextProviderRegistry, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 88d02f60056..ea88afa5579 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -13,7 +13,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { CodeBlockContextProviderRegistry, IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidgetService, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserAction } from 'vs/workbench/contrib/chat/common/chatService'; @@ -96,6 +96,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatService private readonly _chatService: IChatService, + @ICodeBlockContextProviderService private readonly _codeBlockContextProviderService: ICodeBlockContextProviderService ) { super(); @@ -118,7 +119,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } - this._register(CodeBlockContextProviderRegistry.registerProvider({ + this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: (editor) => { const chatWidget = this.chatWidget; if (!chatWidget) { From 4b80026b0b794f2e82e51f587c80c49dd9a2b77c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 08:46:11 -0700 Subject: [PATCH 112/141] pass in isCodeBlockEditable to updateChatMessage --- .../inlineChat/browser/inlineChatWidget.ts | 24 ++++++++----------- .../chat/browser/terminalChatController.ts | 4 ++-- .../chat/browser/terminalChatWidget.ts | 3 +-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 8e729d360ae..77325ccd8c2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -97,11 +97,6 @@ export interface IInlineChatWidgetConstructionOptions { * The men that rendered in the lower right corner, use for feedback */ feedbackMenuId: MenuId; - - /** - * Whether the code blocks are editable. - */ - editableCodeBlock?: boolean; } export interface IInlineChatMessage { @@ -177,7 +172,7 @@ export class InlineChatWidget { private readonly _codeBlockModelCollection: CodeBlockModelCollection; constructor( - private readonly _options: IInlineChatWidgetConstructionOptions, + options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -192,7 +187,7 @@ export class InlineChatWidget { const hoverDelegate = this._store.add(createInstantHoverDelegate()); // input editor logic - this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: _options.inputMenuId, telemetrySource: _options.telemetrySource, hoverDelegate }); + this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: options.inputMenuId, telemetrySource: options.telemetrySource, hoverDelegate }); this._elements.body.replaceChild(this._inputWidget.domNode, this._elements.content); this._store.add(this._inputWidget); this._store.add(this._inputWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); @@ -224,15 +219,15 @@ export class InlineChatWidget { this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: _options.telemetrySource, + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu; - const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options; + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); @@ -247,7 +242,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -366,7 +361,8 @@ export class InlineChatWidget { updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; - updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined { this._chatMessageDisposables.clear(); this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; @@ -378,7 +374,7 @@ export class InlineChatWidget { const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); - const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true, editableCodeBlock: this._options.editableCodeBlock }; + const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true, editableCodeBlock: isCodeBlockEditable ?? false }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index ea88afa5579..62438452ee4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -305,10 +305,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._lastResponseContent = responseContent; if (this._currentRequest) { this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); - this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id, providerId: 'terminal' }); + const containsCode = responseContent.includes('```'); + this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id, providerId: 'terminal' }, false, containsCode); // the message grows in height, be sure to update top position so it doesn't go below the terminal this._chatWidget?.value.layoutVertically(); - const containsCode = responseContent.includes('```'); this._responseContainsCodeBlockContextKey.set(containsCode); this._chatWidget?.value.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 26af5678964..78a3e2e1015 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -50,8 +50,7 @@ export class TerminalChatWidget extends Disposable { widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, statusMenuId: MENU_TERMINAL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - telemetrySource: 'terminal-inline-chat', - editableCodeBlock: true + telemetrySource: 'terminal-inline-chat' } ); this._reset(); From 57d282b90956d8c766dcd201ba1f2ed7eb0646bd Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Mon, 11 Mar 2024 08:53:22 -0700 Subject: [PATCH 113/141] update distro commit (#207337) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index caf83733802..22439e69808 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "9cacfa07a0a807c640d14bda9eac544fd3295a0f", + "distro": "cace2b0e305fab89810eb1a0a2c98c50838aec5e", "author": { "name": "Microsoft Corporation" }, From 64f67b50e7c5b71d84f118eb3d5d71e98bb8359a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 08:54:27 -0700 Subject: [PATCH 114/141] get correct order for actions in toolbar --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index edef26447ea..a80cb518f40 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -88,7 +88,8 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation' + group: 'navigation', + order: 1 } }); } @@ -189,6 +190,7 @@ export function registerChatCodeBlockActions() { id: MenuId.ChatCodeBlock, group: 'navigation', when: CONTEXT_IN_CHAT_SESSION, + order: 2 }, keybinding: { when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), From b2fad5399a92683438f2c8739a9625278839199a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 09:10:09 -0700 Subject: [PATCH 115/141] move to separate file --- .../contrib/chat/browser/chat.contribution.ts | 3 ++- src/vs/workbench/contrib/chat/browser/chat.ts | 14 +------------ .../codeBlockContextProviderService.ts | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 5e2d466ff78..08d1a47a904 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -32,7 +32,7 @@ import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/act import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { CodeBlockContextProviderService, IChatAccessibilityService, IChatWidget, IChatWidgetService, ICodeBlockContextProviderService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidget, IChatWidgetService, ICodeBlockContextProviderService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; @@ -58,6 +58,7 @@ import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/c import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; +import { CodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index f3f4301408e..d66b9f4a468 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; @@ -147,15 +147,3 @@ export interface ICodeBlockContextProviderService { readonly providers: ICodeBlockActionContextProvider[]; registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable; } -export class CodeBlockContextProviderService implements ICodeBlockContextProviderService { - declare _serviceBrand: undefined; - private readonly _providers = new Map(); - - get providers(): ICodeBlockActionContextProvider[] { - return [...this._providers.values()]; - } - registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { - this._providers.set(id, provider); - return toDisposable(() => this._providers.delete(id)); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts b/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts new file mode 100644 index 00000000000..fdfb906e5f1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeBlockActionContextProvider, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; + +export class CodeBlockContextProviderService implements ICodeBlockContextProviderService { + declare _serviceBrand: undefined; + private readonly _providers = new Map(); + + get providers(): ICodeBlockActionContextProvider[] { + return [...this._providers.values()]; + } + registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { + this._providers.set(id, provider); + return toDisposable(() => this._providers.delete(id)); + } +} From fc62e2a62f2e99bb0262d4f8b9c87ad5c43c4b5c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 11 Mar 2024 17:33:34 +0100 Subject: [PATCH 116/141] BUILD: unblock build by skipping leaking test (#207340) --- src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts index 5e959538012..9f7b6977176 100644 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -65,7 +65,7 @@ class TestNativeWindow extends NativeWindow { protected override enableMultiWindowAwareTimeout(): void { } } -suite('NativeWindow:resolveExternal', () => { +suite.skip('NativeWindow:resolveExternal', () => { const disposables = new DisposableStore(); const tunnelMock = new TunnelMock(); let window: TestNativeWindow; From ab77280f550c05b2c99d9853dc44ec9586c9d26c Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:49:27 -0500 Subject: [PATCH 117/141] try to clear transient editors on EditorViewStateManager restore (#206380) * try to clear transient editors on EditorViewStateManager restore * add onWillHide to quickpick * adopt onwillhide * Update src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts Co-authored-by: Benjamin Pasero * Update src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts Co-authored-by: Benjamin Pasero * refactoring for pr feedback * rename to PickerEditorState and ensure dispose will clean up * add openTransientEditor * Update src/vs/workbench/browser/quickaccess.ts Co-authored-by: Benjamin Pasero * Update src/vs/workbench/browser/quickaccess.ts Co-authored-by: Benjamin Pasero * use editorService.closeEditors * don't close current file * delete pickerEditorState onClose and check for dirty editors before closing * editors - do not turn non-transient already opened editors transient * let pinning clear transient * :lipstick: * :lipstick: * improve closing of editors --------- Co-authored-by: Benjamin Pasero --- src/vs/platform/editor/common/editor.ts | 3 + .../browser/parts/editor/editorGroupView.ts | 9 +-- src/vs/workbench/browser/quickaccess.ts | 69 ++++++++++++++----- .../common/editor/editorGroupModel.ts | 19 ++--- .../search/browser/anythingQuickAccess.ts | 17 +++-- .../quickTextSearch/textSearchQuickAccess.ts | 21 +++--- .../links/browser/terminalLinkQuickpick.ts | 18 ++--- .../editor/common/editorGroupsService.ts | 13 ---- .../test/browser/editorGroupsService.test.ts | 65 +++++++++-------- .../test/browser/quickAccess.test.ts | 60 +++++++++++++++- .../test/browser/workbenchTestServices.ts | 1 - 11 files changed, 187 insertions(+), 108 deletions(-) diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 51060787d4a..159bea6fc8e 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -296,6 +296,9 @@ export interface IEditorOptions { * This option is meant to be used only when the editor is used for a short * period of time, for example when opening a preview of the editor from a * picker control in the background while navigating through results of the picker. + * + * Note: an editor that is already opened in a group that is not transient, will + * not turn transient. */ transient?: boolean; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 69bfb90f5d4..7baf78b613f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -799,7 +799,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // that the transient state is not staying around when // the user interacts with the editor. - this.setTransient(this.activeEditor, false); + this.model.setTransient(this.activeEditor, false); } } @@ -1034,13 +1034,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - setTransient(candidate: EditorInput | undefined, transient: boolean): void { - const editor = candidate ?? this.activeEditor; - if (editor) { - this.model.setTransient(editor, transient); - } - } - //#endregion //#region openEditor() diff --git a/src/vs/workbench/browser/quickaccess.ts b/src/vs/workbench/browser/quickaccess.ts index 6b9e07abe7a..aec2065963d 100644 --- a/src/vs/workbench/browser/quickaccess.ts +++ b/src/vs/workbench/browser/quickaccess.ts @@ -8,12 +8,14 @@ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/con import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { Disposable } from 'vs/base/common/lifecycle'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; +import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import { IUntitledTextResourceEditorInput, IUntypedEditorInput, GroupIdentifier, IEditorPane } from 'vs/workbench/common/editor'; export const inQuickPickContextKeyValue = 'inQuickOpen'; export const InQuickPickContextKey = new RawContextKey(inQuickPickContextKeyValue, false, localize('inQuickOpen', "Whether keyboard focus is inside the quick open control")); @@ -51,14 +53,21 @@ export function getQuickNavigateHandler(id: string, next?: boolean): ICommandHan quickInputService.navigate(!!next, quickNavigate); }; } -export class EditorViewState { +export class PickerEditorState extends Disposable { private _editorViewState: { editor: EditorInput; group: IEditorGroup; state: ICodeEditorViewState | IDiffEditorViewState | undefined; } | undefined = undefined; - constructor(private readonly editorService: IEditorService) { } + private readonly openedTransientEditors = new Set(); // editors that were opened between set and restore + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService + ) { + super(); + } set(): void { if (this._editorViewState) { @@ -73,27 +82,55 @@ export class EditorViewState { state: getIEditor(activeEditorPane.getControl())?.saveViewState() ?? undefined, }; } + } - async restore(shouldCloseCurrEditor = false): Promise { + /** + * Open a transient editor such that it may be closed when the state is restored. + * Note that, when the state is restored, if the editor is no longer transient, it will not be closed. + */ + async openTransientEditor(editor: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IUntypedEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise { + editor.options = { ...editor.options, transient: true }; + + const editorPane = await this.editorService.openEditor(editor, group); + if (editorPane?.input && editorPane.input !== this._editorViewState?.editor && editorPane.group.isTransient(editorPane.input)) { + this.openedTransientEditors.add(editorPane.input); + } + + return editorPane; + } + + async restore(): Promise { if (this._editorViewState) { - const options: IEditorOptions = { - viewState: this._editorViewState.state, - preserveFocus: true /* import to not close the picker as a result */ - }; - if (shouldCloseCurrEditor) { - const activeEditorPane = this.editorService.activeEditorPane; - const currEditor = activeEditorPane?.input; - if (currEditor && currEditor !== this._editorViewState.editor && activeEditorPane?.group.isPinned(currEditor) !== true) { - await activeEditorPane.group.closeEditor(currEditor); + for (const editor of this.openedTransientEditors) { + if (editor.isDirty()) { + continue; + } + + for (const group of this.editorGroupsService.groups) { + if (group.isTransient(editor)) { + await group.closeEditor(editor, { preserveFocus: true }); + } } } - await this._editorViewState.group.openEditor(this._editorViewState.editor, options); + await this._editorViewState.group.openEditor(this._editorViewState.editor, { + viewState: this._editorViewState.state, + preserveFocus: true // important to not close the picker as a result + }); + + this.reset(); } } reset() { this._editorViewState = undefined; + this.openedTransientEditors.clear(); + } + + override dispose(): void { + super.dispose(); + + this.reset(); } } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 17c5ab59c26..60047f630e3 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -369,6 +369,11 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.splice(targetIndex, false, newEditor); } + // Handle transient + if (makeTransient) { + this.doSetTransient(newEditor, targetIndex, true); + } + // Handle preview if (!makePinned) { @@ -385,11 +390,6 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.preview = newEditor; } - // Handle transient - if (makeTransient) { - this.doSetTransient(newEditor, targetIndex, true); - } - // Listeners this.registerEditorListeners(newEditor); @@ -416,14 +416,14 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { else { const [existingEditor, existingEditorIndex] = existingEditorAndIndex; + // Update transient (existing editors do not turn transient if they were not before) + this.doSetTransient(existingEditor, existingEditorIndex, makeTransient === false ? false : this.isTransient(existingEditor)); + // Pin it if (makePinned) { this.doPin(existingEditor, existingEditorIndex); } - // Update transient - this.doSetTransient(existingEditor, existingEditorIndex, makeTransient); - // Activate it if (makeActive) { this.doSetActive(existingEditor, existingEditorIndex); @@ -726,6 +726,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return; // can only pin a preview editor } + // Clear Transient + this.setTransient(editor, false); + // Convert the preview editor to be a pinned editor this.preview = null; diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index a85e27af833..12523447b89 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -41,7 +41,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { ResourceMap } from 'vs/base/common/map'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue, Extensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; -import { EditorViewState, IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess'; +import { PickerEditorState, IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess'; import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor } from 'vs/editor/common/editorCommon'; @@ -83,11 +83,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; - editorViewState: EditorViewState; + editorViewState = this._register(this.instantiationService.createInstance(PickerEditorState)); scorerCache: FuzzyScorerCache = Object.create(null); fileQueryCache: FileQueryCacheState | undefined = undefined; @@ -100,8 +100,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider): void { @@ -129,7 +132,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - await this._editorService.openEditor({ + await this.editorViewState.openTransientEditor({ resource: itemMatch.parent().resource, - options: { transient: true, preserveFocus: true, revealIfOpened: true, ignoreError: true, selection: itemMatch.range() } + options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection: itemMatch.range() } }); }); } })); - - disposables.add(Event.once(picker.onDidHide)(({ reason }) => { + disposables.add(Event.once(picker.onWillHide)(({ reason }) => { // Restore view state upon cancellation if we changed it // but only when the picker was closed via explicit user // gesture and not e.g. when focus was lost because that // could mean the user clicked into the editor directly. if (reason === QuickInputHideReason.Gesture) { - this.editorViewState.restore(true); + this.editorViewState.restore(); } + })); + + disposables.add(Event.once(picker.onDidHide)(({ reason }) => { this.searchModel.searchResult.toggleHighlights(false); })); diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts index c35df5218e3..423aa2430b2 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts @@ -15,17 +15,17 @@ import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/brows import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import type { TerminalLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLink'; import { Sequencer, timeout } from 'vs/base/common/async'; -import { EditorViewState } from 'vs/workbench/browser/quickaccess'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; import { ILabelService } from 'vs/platform/label/common/label'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class TerminalLinkQuickpick extends DisposableStore { private readonly _editorSequencer = new Sequencer(); - private readonly _editorViewState: EditorViewState; + private readonly _editorViewState: PickerEditorState; private _instance: ITerminalInstance | IDetachedTerminalInstance | undefined; @@ -33,13 +33,13 @@ export class TerminalLinkQuickpick extends DisposableStore { readonly onDidRequestMoreLinks = this._onDidRequestMoreLinks.event; constructor( - @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IInstantiationService instantiationService: IInstantiationService ) { super(); - this._editorViewState = new EditorViewState(_editorService); + this._editorViewState = this.add(instantiationService.createInstance(PickerEditorState)); } async show(instance: ITerminalInstance | IDetachedTerminalInstance, links: { viewport: IDetectedLinks; all: Promise }): Promise { @@ -145,7 +145,7 @@ export class TerminalLinkQuickpick extends DisposableStore { // gesture and not e.g. when focus was lost because that // could mean the user clicked into the editor directly. if (reason === QuickInputHideReason.Gesture) { - this._editorViewState.restore(true); + this._editorViewState.restore(); } disposables.dispose(); if (pick.selectedItems.length === 0) { @@ -266,9 +266,9 @@ export class TerminalLinkQuickpick extends DisposableStore { this._editorViewState.set(); this._editorSequencer.queue(async () => { - await this._editorService.openEditor({ + await this._editorViewState.openTransientEditor({ resource: link.uri, - options: { transient: true, preserveFocus: true, revealIfOpened: true, ignoreError: true, selection, } + options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection, } }); }); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index fd2f81f70e9..7db5884efe3 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -837,19 +837,6 @@ export interface IEditorGroup { */ unstickEditor(editor?: EditorInput): void; - /** - * A transient editor will attempt to appear as preview and certain components - * (such as history tracking) may decide to ignore the editor when it becomes - * active. - * This option is meant to be used only when the editor is used for a short - * period of time, for example when opening a preview of the editor from a - * picker control in the background while navigating through results of the picker. - * - * @param editor the editor to update transient state, or the currently active editor - * if unspecified. - */ - setTransient(editor: EditorInput | undefined, transient: boolean): void; - /** * Whether this editor group should be locked or not. * 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 f1e25f4a355..56317e9b39d 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -477,7 +477,6 @@ suite('EditorGroupsService', () => { const editorCloseEvents: IGroupModelChangeEvent[] = []; let editorPinCounter = 0; let editorStickyCounter = 0; - let editorTransientCounter = 0; let editorCapabilitiesCounter = 0; const editorGroupModelChangeListener = group.onDidModelChange(e => { if (e.kind === GroupModelChangeKind.EDITOR_OPEN) { @@ -490,9 +489,6 @@ suite('EditorGroupsService', () => { } else if (e.kind === GroupModelChangeKind.EDITOR_STICKY) { assert.ok(e.editor); editorStickyCounter++; - } else if (e.kind === GroupModelChangeKind.EDITOR_TRANSIENT) { - assert.ok(e.editor); - editorTransientCounter++; } else if (e.kind === GroupModelChangeKind.EDITOR_CAPABILITIES) { assert.ok(e.editor); editorCapabilitiesCounter++; @@ -597,15 +593,6 @@ suite('EditorGroupsService', () => { group.unstickEditor(input); assert.strictEqual(editorStickyCounter, 2); - assert.strictEqual(group.isTransient(input), false); - assert.strictEqual(editorTransientCounter, 0); - group.setTransient(input, true); - assert.strictEqual(group.isTransient(input), true); - assert.strictEqual(editorTransientCounter, 1); - group.setTransient(input, false); - assert.strictEqual(group.isTransient(input), false); - assert.strictEqual(editorTransientCounter, 2); - editorCloseListener.dispose(); editorWillCloseListener.dispose(); editorDidCloseListener.dispose(); @@ -1835,30 +1822,42 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const inputTransient = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input, { pinned: true }); - await group.openEditor(inputInactive, { inactive: true }); - - assert.strictEqual(group.isTransient(input), false); - assert.strictEqual(group.isTransient(inputInactive), false); - - group.setTransient(input, true); - - assert.strictEqual(group.isTransient(input), true); - assert.strictEqual(group.isTransient(inputInactive), false); - - group.setTransient(input, false); - - assert.strictEqual(group.isTransient(input), false); - assert.strictEqual(group.isTransient(inputInactive), false); - - const inputTransient = createTestFileEditorInput(URI.file('foo/bar/transient'), TEST_EDITOR_INPUT_ID); - await group.openEditor(inputTransient, { transient: true }); + + assert.strictEqual(group.isTransient(input), false); assert.strictEqual(group.isTransient(inputTransient), true); - await group.openEditor(inputTransient, {}); + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputTransient, { transient: true }); + + assert.strictEqual(group.isTransient(inputTransient), true); + + await group.openEditor(inputTransient, { transient: false }); + assert.strictEqual(group.isTransient(inputTransient), false); + + await group.openEditor(inputTransient, { transient: true }); + assert.strictEqual(group.isTransient(inputTransient), false); // cannot make a non-transient editor transient when already opened + }); + + test('transient editors - pinning clears transient', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputTransient = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputTransient, { transient: true }); + + assert.strictEqual(group.isTransient(input), false); + assert.strictEqual(group.isTransient(inputTransient), true); + + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputTransient, { pinned: true, transient: true }); + assert.strictEqual(group.isTransient(inputTransient), false); }); @@ -1882,7 +1881,7 @@ suite('EditorGroupsService', () => { await group.openEditor(input2, { transient: true }); assert.strictEqual(group.isPinned(input2), false); - group.setTransient(input2, false); + group.focus(); assert.strictEqual(group.isPinned(input2), true); }); diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts index 3130a990ac5..9d71c84ccf3 100644 --- a/src/vs/workbench/test/browser/quickAccess.test.ts +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -8,16 +8,23 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestServiceAccessor, workbenchInstantiationService, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { DisposableStore, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { PickerQuickAccessProvider, FastAndSlowPicks } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { URI } from 'vs/base/common/uri'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { Range } from 'vs/editor/common/core/range'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; suite('QuickAccess', () => { let disposables: DisposableStore; - let instantiationService: IInstantiationService; + let instantiationService: TestInstantiationService; let accessor: TestServiceAccessor; let providerDefaultCalled = false; @@ -334,4 +341,51 @@ suite('QuickAccess', () => { restore(); }); + + test('PickerEditorState can properly restore editors', async () => { + + const part = await createEditorPart(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, part); + + const editorService = disposables.add(instantiationService.createInstance(EditorService, undefined)); + instantiationService.stub(IEditorService, editorService); + + const editorViewState = disposables.add(instantiationService.createInstance(PickerEditorState)); + disposables.add(part); + disposables.add(editorService); + + const input1 = { + resource: URI.parse('foo://bar1'), + options: { + pinned: true, preserveFocus: true, selection: new Range(1, 0, 1, 3) + } + }; + const input2 = { + resource: URI.parse('foo://bar2'), + options: { + pinned: true, selection: new Range(1, 0, 1, 3) + } + }; + const input3 = { + resource: URI.parse('foo://bar3') + }; + const input4 = { + resource: URI.parse('foo://bar4') + }; + + const editor = await editorService.openEditor(input1); + assert.strictEqual(editor, editorService.activeEditorPane); + editorViewState.set(); + await editorService.openEditor(input2); + await editorViewState.openTransientEditor(input3); + await editorViewState.openTransientEditor(input4); + await editorViewState.restore(); + + assert.strictEqual(part.activeGroup.activeEditor?.resource, input1.resource); + assert.deepStrictEqual(part.activeGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map(e => e.resource), [input1.resource, input2.resource]); + if (part.activeGroup.activeEditorPane?.getSelection) { + assert.deepStrictEqual(part.activeGroup.activeEditorPane?.getSelection(), input1.options.selection); + } + await part.activeGroup.closeAllEditors(); + }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6fcf99d03f3..c37caab7092 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -918,7 +918,6 @@ export class TestEditorGroupView implements IEditorGroupView { pinEditor(_editor?: EditorInput): void { } stickEditor(editor?: EditorInput | undefined): void { } unstickEditor(editor?: EditorInput | undefined): void { } - setTransient(editor: EditorInput | undefined, transient: boolean): void { } lock(locked: boolean): void { } focus(): void { } get scopedContextKeyService(): IContextKeyService { throw new Error('not implemented'); } From d8880a78e9f88bbb117e4cde079b39838a924349 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 11 Mar 2024 09:55:28 -0700 Subject: [PATCH 118/141] cli: propagate exit code from cli.js (#207343) Fixes #204067 --- resources/win32/bin/code.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/win32/bin/code.cmd b/resources/win32/bin/code.cmd index 9da8ab4f7b8..7e7b92c9eb7 100644 --- a/resources/win32/bin/code.cmd +++ b/resources/win32/bin/code.cmd @@ -3,4 +3,5 @@ setlocal set VSCODE_DEV= set ELECTRON_RUN_AS_NODE=1 "%~dp0..\@@NAME@@.exe" "%~dp0..\resources\app\out\cli.js" %* +IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL% endlocal From e08191789b6e77435d76d294079c47c6f3aab495 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 11 Mar 2024 11:06:05 -0700 Subject: [PATCH 119/141] Pick up latest TS for building VS Code (#207347) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 22439e69808..a889d68b52e 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240307", + "typescript": "^5.5.0-dev.20240311", "typescript-formatter": "7.1.0", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index a97562951bf..8dfa062b75b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9643,10 +9643,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240307: - version "5.5.0-dev.20240307" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240307.tgz#dcd10b4a4d5a274f40cdfa91c75ae7a940ade469" - integrity sha512-9QgBAVHg1keeajSXIqq1ngFy3yTA0um5YWoLNVSfPtIwqqYoVqRjmh4oJKJL4YM15C2HY8IXL/UEUBIjXm/tjg== +typescript@^5.5.0-dev.20240311: + version "5.5.0-dev.20240311" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240311.tgz#98eb4774ff5dc21d821e8b0c60e06ad0c0c7e693" + integrity sha512-Cdp0eYgn/19lkcrq7WCqQxmnvCqvuJrd/jGhm1HyPMSYVTGzjxVP0NfXr2A4YVS12IAipt1uO4zgAJeLlYG2JA== typical@^4.0.0: version "4.0.0" From d4b4c327798c27807c2710fa15a420c5dd17429d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Tue, 12 Mar 2024 02:12:26 +0800 Subject: [PATCH 120/141] Fix Copy/Cut command not working in webview (#206529) * Fix Copy/Cut command not working in webview * Update Content-Security-Policy of index.html * Update csp --------- Co-authored-by: Matt Bierner --- .../workbench/contrib/webview/browser/pre/index-no-csp.html | 3 ++- src/vs/workbench/contrib/webview/browser/pre/index.html | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 45a4086cc9e..5e094fc4ccc 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -46,7 +46,8 @@ const interval = 250; let isFocused = document.hasFocus(); setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); + const target = getActiveFrame(); + const isCurrentlyFocused = document.hasFocus() || !!(target && target.contentDocument && target.contentDocument.body.classList.contains('vscode-context-menu-visible')); if (isCurrentlyFocused === isFocused) { return; } diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 0bf6401663d..fa7b15e39c8 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-bQPwjO6bLiyf6v9eDVtAI67LrfonA1w49aFkRXBy4/g=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> { - const isCurrentlyFocused = document.hasFocus(); + const target = getActiveFrame(); + const isCurrentlyFocused = document.hasFocus() || !!(target && target.contentDocument && target.contentDocument.body.classList.contains('vscode-context-menu-visible')); if (isCurrentlyFocused === isFocused) { return; } From bb55abf616e5df6220cb74a89a21a434a05afdb2 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 11 Mar 2024 19:30:40 +0100 Subject: [PATCH 121/141] sticky scroll: listen to folding range provider change events (#207209) * sticky scroll: listen to folding range provider change events * fix undisposed stickyModelProvider * dispose the providers in the StickyModelProvider --- .../browser/stickyScrollModelProvider.ts | 174 ++++++++---------- .../browser/stickyScrollProvider.ts | 45 +++-- 2 files changed, 100 insertions(+), 119 deletions(-) diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index 7f7b99c5a9d..231052024c6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -3,23 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { FoldingController, RangesLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; -import { ITextModel } from 'vs/editor/common/model'; import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/browser/syntaxRangeProvider'; import { IndentRangeProvider } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { StickyElement, StickyModel, StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; import { Iterable } from 'vs/base/common/iterator'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; enum ModelProvider { OUTLINE_MODEL = 'outlineModel', @@ -33,16 +32,14 @@ enum Status { CANCELED } -export interface IStickyModelProvider { +export interface IStickyModelProvider extends IDisposable { /** * Method which updates the sticky model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @returns the sticky model */ - update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise; + update(token: CancellationToken): Promise; } export class StickyModelProvider extends Disposable implements IStickyModelProvider { @@ -53,33 +50,33 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi private readonly _updateOperation: DisposableStore = this._register(new DisposableStore()); constructor( - private readonly _editor: ICodeEditor, - @ILanguageConfigurationService readonly _languageConfigurationService: ILanguageConfigurationService, + private readonly _editor: IActiveCodeEditor, + onProviderUpdate: () => void, + @IInstantiationService readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService readonly _languageFeaturesService: ILanguageFeaturesService, - defaultModel: string ) { super(); - const stickyModelFromCandidateOutlineProvider = new StickyModelFromCandidateOutlineProvider(_languageFeaturesService); - const stickyModelFromSyntaxFoldingProvider = new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, _languageFeaturesService); - const stickyModelFromIndentationFoldingProvider = new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService); - - switch (defaultModel) { + switch (this._editor.getOption(EditorOption.stickyScroll).defaultModel) { case ModelProvider.OUTLINE_MODEL: - this._modelProviders.push(stickyModelFromCandidateOutlineProvider); - this._modelProviders.push(stickyModelFromSyntaxFoldingProvider); - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); - break; + this._modelProviders.push(new StickyModelFromCandidateOutlineProvider(this._editor, _languageFeaturesService)); + // fall through case ModelProvider.FOLDING_PROVIDER_MODEL: - this._modelProviders.push(stickyModelFromSyntaxFoldingProvider); - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); - break; + this._modelProviders.push(new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, onProviderUpdate, _languageFeaturesService)); + // fall through case ModelProvider.INDENTATION_MODEL: - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); + this._modelProviders.push(new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService)); break; } } + public override dispose(): void { + this._modelProviders.forEach(provider => provider.dispose()); + this._updateOperation.clear(); + this._cancelModelPromise(); + super.dispose(); + } + private _cancelModelPromise(): void { if (this._modelPromise) { this._modelPromise.cancel(); @@ -87,7 +84,7 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi } } - public async update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise { + public async update(token: CancellationToken): Promise { this._updateOperation.clear(); this._updateOperation.add({ @@ -101,11 +98,7 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi return await this._updateScheduler.trigger(async () => { for (const modelProvider of this._modelProviders) { - const { statusPromise, modelPromise } = modelProvider.computeStickyModel( - textModel, - textModelVersionId, - token - ); + const { statusPromise, modelPromise } = modelProvider.computeStickyModel(token); this._modelPromise = modelPromise; const status = await statusPromise; if (this._modelPromise !== modelPromise) { @@ -127,26 +120,24 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi } } -interface IStickyModelCandidateProvider { +interface IStickyModelCandidateProvider extends IDisposable { get stickyModel(): StickyModel | null; - get provider(): LanguageFeatureRegistry | null; - /** * Method which computes the sticky model and returns a status to signal whether the sticky model has been successfully found - * @param textmodel text-model of the editor - * @param modelVersionId version ID of the text-model * @param token cancellation token * @returns a promise of a status indicating whether the sticky model has been successfully found as well as the model promise */ - computeStickyModel(textmodel: ITextModel, modelVersionId: number, token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null }; + computeStickyModel(token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null }; } -abstract class StickyModelCandidateProvider implements IStickyModelCandidateProvider { +abstract class StickyModelCandidateProvider extends Disposable implements IStickyModelCandidateProvider { protected _stickyModel: StickyModel | null = null; - constructor() { } + constructor(protected readonly _editor: IActiveCodeEditor) { + super(); + } get stickyModel(): StickyModel | null { return this._stickyModel; @@ -157,13 +148,11 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP return Status.INVALID; } - public abstract get provider(): LanguageFeatureRegistry | null; - - public computeStickyModel(textModel: ITextModel, modelVersionId: number, token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null } { - if (token.isCancellationRequested || !this.isProviderValid(textModel)) { + public computeStickyModel(token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null } { + if (token.isCancellationRequested || !this.isProviderValid()) { return { statusPromise: this._invalid(), modelPromise: null }; } - const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(textModel, modelVersionId, token)); + const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(token)); return { statusPromise: providerModelPromise.then(providerModel => { @@ -174,7 +163,7 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP if (token.isCancellationRequested) { return Status.CANCELED; } - this._stickyModel = this.createStickyModel(textModel, modelVersionId, token, providerModel); + this._stickyModel = this.createStickyModel(token, providerModel); return Status.VALID; }).then(undefined, (err) => { onUnexpectedError(err); @@ -190,57 +179,49 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP * @param model model returned by the provider * @returns boolean indicating whether the model is valid */ - protected isModelValid(model: any): boolean { + protected isModelValid(model: T): boolean { return true; } /** * Method which checks whether the provider is valid before applying it to find the provider model. * This method by default returns true. - * @param textModel text-model of the editor * @returns boolean indicating whether the provider is valid */ - protected isProviderValid(textModel: ITextModel): boolean { + protected isProviderValid(): boolean { return true; } /** * Abstract method which creates the model from the provider and returns the provider model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @returns the model returned by the provider */ - protected abstract createModelFromProvider(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise; + protected abstract createModelFromProvider(token: CancellationToken): Promise; /** * Abstract method which computes the sticky model from the model returned by the provider and returns the sticky model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @param model model returned by the provider * @returns the sticky model */ - protected abstract createStickyModel(textModel: ITextModel, textModelVersionId: number, token: CancellationToken, model: T): StickyModel; + protected abstract createStickyModel(token: CancellationToken, model: T): StickyModel; } class StickyModelFromCandidateOutlineProvider extends StickyModelCandidateProvider { - constructor(@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { - super(); + constructor(_editor: IActiveCodeEditor, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { + super(_editor); } - public get provider(): LanguageFeatureRegistry | null { - return this._languageFeaturesService.documentSymbolProvider; + protected createModelFromProvider(token: CancellationToken): Promise { + return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, this._editor.getModel(), token); } - protected createModelFromProvider(textModel: ITextModel, modelVersionId: number, token: CancellationToken): Promise { - return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, textModel, token); - } - - protected createStickyModel(textModel: TextModel, modelVersionId: number, token: CancellationToken, model: OutlineModel): StickyModel { + protected createStickyModel(token: CancellationToken, model: OutlineModel): StickyModel { const { stickyOutlineElement, providerID } = this._stickyModelFromOutlineModel(model, this._stickyModel?.outlineProviderId); - return new StickyModel(textModel.uri, modelVersionId, stickyOutlineElement, providerID); + const textModel = this._editor.getModel(); + return new StickyModel(textModel.uri, textModel.getVersionId(), stickyOutlineElement, providerID); } protected override isModelValid(model: OutlineModel): boolean { @@ -334,14 +315,15 @@ abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandid protected _foldingLimitReporter: RangesLimitReporter; - constructor(editor: ICodeEditor) { - super(); + constructor(editor: IActiveCodeEditor) { + super(editor); this._foldingLimitReporter = new RangesLimitReporter(editor); } - protected createStickyModel(textModel: ITextModel, modelVersionId: number, token: CancellationToken, model: FoldingRegions): StickyModel { + protected createStickyModel(token: CancellationToken, model: FoldingRegions): StickyModel { const foldingElement = this._fromFoldingRegions(model); - return new StickyModel(textModel.uri, modelVersionId, foldingElement, undefined); + const textModel = this._editor.getModel(); + return new StickyModel(textModel.uri, textModel.getVersionId(), foldingElement, undefined); } protected override isModelValid(model: FoldingRegions): boolean { @@ -387,49 +369,41 @@ abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandid class StickyModelFromCandidateIndentationFoldingProvider extends StickyModelFromCandidateFoldingProvider { + private readonly provider: IndentRangeProvider; + constructor( - editor: ICodeEditor, + editor: IActiveCodeEditor, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService) { super(editor); + + this.provider = this._register(new IndentRangeProvider(editor.getModel(), this._languageConfigurationService, this._foldingLimitReporter)); } - public get provider(): LanguageFeatureRegistry | null { - return null; - } - - protected async createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { - const provider = new IndentRangeProvider(textModel, this._languageConfigurationService, this._foldingLimitReporter); - try { - return await provider.compute(token); - } finally { - provider.dispose(); - } + protected override async createModelFromProvider(token: CancellationToken): Promise { + return this.provider.compute(token); } } class StickyModelFromCandidateSyntaxFoldingProvider extends StickyModelFromCandidateFoldingProvider { - constructor(editor: ICodeEditor, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { + private readonly provider: SyntaxRangeProvider | undefined; + + constructor(editor: IActiveCodeEditor, + onProviderUpdate: () => void, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService + ) { super(editor); - } - - public get provider(): LanguageFeatureRegistry | null { - return this._languageFeaturesService.foldingRangeProvider; - } - - protected override isProviderValid(textModel: TextModel): boolean { - const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, textModel); - return selectedProviders.length > 0; - } - - protected createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { - const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, textModel); - const provider = new SyntaxRangeProvider(textModel, selectedProviders, () => this.createModelFromProvider(textModel, modelVersionId, token), this._foldingLimitReporter, undefined); - try { - return provider.compute(token); - } finally { - provider.dispose(); + const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, editor.getModel()); + if (selectedProviders.length > 0) { + this.provider = this._register(new SyntaxRangeProvider(editor.getModel(), selectedProviders, onProviderUpdate, this._foldingLimitReporter, undefined)); } } + + protected override isProviderValid(): boolean { + return this.provider !== undefined; + } + + protected override async createModelFromProvider(token: CancellationToken): Promise { + return this.provider?.compute(token) ?? null; + } } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 3388380f97c..705ef76489e 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CancellationToken, CancellationTokenSource, } from 'vs/base/common/cancellation'; -import { EditorOption, IEditorStickyScrollOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Range } from 'vs/editor/common/core/range'; import { binarySearch } from 'vs/base/common/arrays'; @@ -45,7 +45,6 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi private readonly _updateSoon: RunOnceScheduler; private readonly _sessionStore: DisposableStore; - private _options: Readonly> | null = null; private _model: StickyModel | null = null; private _cts: CancellationTokenSource | null = null; private _stickyModelProvider: IStickyModelProvider | null = null; @@ -69,26 +68,18 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi } private readConfiguration() { - - this._stickyModelProvider = null; this._sessionStore.clear(); - this._options = this._editor.getOption(EditorOption.stickyScroll); - if (!this._options.enabled) { + const options = this._editor.getOption(EditorOption.stickyScroll); + if (!options.enabled) { return; } - this._stickyModelProvider = this._sessionStore.add(new StickyModelProvider( - this._editor, - this._languageConfigurationService, - this._languageFeaturesService, - this._options.defaultModel - )); - this._sessionStore.add(this._editor.onDidChangeModel(() => { // We should not show an old model for a different file, it will always be wrong. // So we clear the model here immediately and then trigger an update. this._model = null; + this.updateStickyModelProvider(); this._onDidChangeStickyScroll.fire(); this.update(); @@ -96,6 +87,11 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi this._sessionStore.add(this._editor.onDidChangeHiddenAreas(() => this.update())); this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._updateSoon.schedule())); this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this.update())); + this._sessionStore.add(toDisposable(() => { + this._stickyModelProvider?.dispose(); + this._stickyModelProvider = null; + })); + this.updateStickyModelProvider(); this.update(); } @@ -103,6 +99,21 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi return this._model?.version; } + private updateStickyModelProvider() { + this._stickyModelProvider?.dispose(); + this._stickyModelProvider = null; + + const editor = this._editor; + if (editor.hasModel()) { + this._stickyModelProvider = new StickyModelProvider( + editor, + () => this._updateSoon.schedule(), + this._languageConfigurationService, + this._languageFeaturesService + ); + } + } + public async update(): Promise { this._cts?.dispose(true); this._cts = new CancellationTokenSource(); @@ -116,11 +127,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi this._model = null; return; } - - const textModel = this._editor.getModel(); - const modelVersionId = textModel.getVersionId(); - - const model = await this._stickyModelProvider.update(textModel, modelVersionId, token); + const model = await this._stickyModelProvider.update(token); if (token.isCancellationRequested) { // the computation was canceled, so do not overwrite the model return; From b732dd9b79d55c5fbf3da958f3b138166514dada Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Mon, 11 Mar 2024 11:38:04 -0700 Subject: [PATCH 122/141] Add option to prompt to save on closing an IW (#207349) * IW scratchpad based on setting * register setting --- .../browser/interactive.contribution.ts | 15 +++++++++- .../browser/interactiveEditorInput.ts | 28 ++++++++++++++++--- .../contrib/notebook/common/notebookCommon.ts | 3 +- .../notebook/common/notebookEditorModel.ts | 3 +- .../notebookEditorModelResolverServiceImpl.ts | 5 ++-- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 9f3899d7961..6ff1ed9b726 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -50,7 +50,7 @@ import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/no import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; -import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -797,3 +797,16 @@ Registry.as(ConfigurationExtensions.Configuration).regis } } }); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'interactiveWindow', + order: 100, + type: 'object', + 'properties': { + [NotebookSetting.InteractiveWindowPromptToSave]: { + type: 'boolean', + default: false, + markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") + } + } +}); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts index 17dc7e1b09b..8dd727d49c6 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts @@ -10,13 +10,14 @@ import { isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IInteractiveDocumentService } from 'vs/workbench/contrib/interactive/browser/interactiveDocumentService'; import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; -import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResolvedNotebookEditorModel, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICompositeNotebookEditorInput, NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -44,6 +45,7 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot } private name: string; + private readonly isScratchpad: boolean; get language() { return this._inputModelRef?.object.textEditorModel.getLanguageId() ?? this._initLanguage; @@ -93,10 +95,12 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot @IInteractiveDocumentService interactiveDocumentService: IInteractiveDocumentService, @IInteractiveHistoryService historyService: IInteractiveHistoryService, @INotebookService private readonly _notebookService: INotebookService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IConfigurationService configurationService: IConfigurationService ) { const input = NotebookEditorInput.getOrCreate(instantiationService, resource, undefined, 'interactive', {}); super(); + this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; this._notebookEditorInput = input; this._register(this._notebookEditorInput); this.name = title ?? InteractiveEditorInput.windowNames[resource.path] ?? paths.basename(resource.path, paths.extname(resource.path)); @@ -130,9 +134,11 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot } override get capabilities(): EditorInputCapabilities { + const scratchPad = this.isScratchpad ? EditorInputCapabilities.Scratchpad : 0; + return EditorInputCapabilities.Untitled | EditorInputCapabilities.Readonly - | EditorInputCapabilities.Scratchpad; + | scratchPad; } private async _resolveEditorModel() { @@ -220,10 +226,24 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot return this.name; } + override isDirty(): boolean { + if (this.isScratchpad) { + return false; + } + + return this._editorModelReference?.isDirty() ?? false; + } + override isModified() { return this._editorModelReference?.isModified() ?? false; } + override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this._editorModelReference && this._editorModelReference.isDirty()) { + await this._editorModelReference.revert(options); + } + } + override dispose() { // we support closing the interactive window without prompt, so the editor model should not be dirty this._editorModelReference?.revert({ soft: true }); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 02c59967a70..37725f005dd 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -950,7 +950,8 @@ export const NotebookSetting = { scrollToRevealCell: 'notebook.scrolling.revealNextCellOnExecute', anchorToFocusedCell: 'notebook.scrolling.experimental.anchorToFocusedCell', cellChat: 'notebook.experimental.cellChat', - notebookVariablesView: 'notebook.experimental.variablesView' + notebookVariablesView: 'notebook.experimental.variablesView', + InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose' } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index f54d5f73092..8f517b56fc1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -52,11 +52,12 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _hasAssociatedFilePath: boolean, readonly viewType: string, private readonly _workingCopyManager: IFileWorkingCopyManager, + scratchpad: boolean, @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService ) { super(); - this.scratchPad = viewType === 'interactive'; + this.scratchPad = scratchpad; } override dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index bbc69d31aa2..0a8a17a170d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { CellUri, IResolvedNotebookEditorModel, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, IResolvedNotebookEditorModel, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -77,7 +77,8 @@ class NotebookModelReferenceCollection extends ReferenceCollection(NotebookSetting.InteractiveWindowPromptToSave) !== true; + const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, scratchPad); const result = await model.load({ limits }); From 0db502e1320287333c65a17c5944a2cdcf5218fc Mon Sep 17 00:00:00 2001 From: Xie Jingyi Date: Tue, 12 Mar 2024 02:41:46 +0800 Subject: [PATCH 123/141] Fix setting editor list item overflow (#206681) --- .../contrib/preferences/browser/media/settingsWidgets.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index a77fedea1d9..89ba7579904 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -18,8 +18,9 @@ .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-sibling, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-value { - white-space: normal; - overflow-wrap: normal; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } .settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-value-checkbox { From dcafd8876c9365ca3fc6fe0f449aaa9a16570c15 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 12:09:47 -0700 Subject: [PATCH 124/141] prefix service name with chat --- .../contrib/accessibility/browser/accessibleView.ts | 4 ++-- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 6 +++--- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 6 +++--- src/vs/workbench/contrib/chat/browser/chat.ts | 4 ++-- .../contrib/chat/browser/codeBlockContextProviderService.ts | 4 ++-- .../terminalContrib/chat/browser/terminalChatController.ts | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index adb8cb85805..862202d57cf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -42,7 +42,7 @@ import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQui import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -173,7 +173,7 @@ export class AccessibleView extends Disposable { @ILayoutService private readonly _layoutService: ILayoutService, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, - @ICodeBlockContextProviderService private readonly _codeBlockContextProviderService: ICodeBlockContextProviderService + @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService ) { super(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index a80cb518f40..d96d7b27bbe 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -26,7 +26,7 @@ import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetService, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; @@ -565,7 +565,7 @@ export function registerChatCodeBlockActions() { function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); - const codeBlockContextProviderService = accessor.get(ICodeBlockContextProviderService); + const chatCodeBlockContextProviderService = accessor.get(IChatCodeBlockContextProviderService); const model = editor.getModel(); if (!model) { return; @@ -574,7 +574,7 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): const widget = chatWidgetService.lastFocusedWidget; const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri); if (!codeBlockInfo) { - for (const provider of codeBlockContextProviderService.providers) { + for (const provider of chatCodeBlockContextProviderService.providers) { const context = provider.getCodeBlockContext(editor); if (context) { return context; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 08d1a47a904..bc95ff7700d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -32,7 +32,7 @@ import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/act import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService, ICodeBlockContextProviderService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; @@ -58,7 +58,7 @@ import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/c import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { CodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; +import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -332,4 +332,4 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); -registerSingleton(ICodeBlockContextProviderService, CodeBlockContextProviderService, InstantiationType.Delayed); +registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index d66b9f4a468..7a7aa2066e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -18,7 +18,7 @@ import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWel export const IChatWidgetService = createDecorator('chatWidgetService'); export const IQuickChatService = createDecorator('quickChatService'); export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); -export const ICodeBlockContextProviderService = createDecorator('codeBlockContextProviderService'); +export const IChatCodeBlockContextProviderService = createDecorator('chatCodeBlockContextProviderService'); export interface IChatWidgetService { @@ -142,7 +142,7 @@ export interface IChatViewPane { export interface ICodeBlockActionContextProvider { getCodeBlockContext(editor?: ICodeEditor): ICodeBlockActionContext | undefined; } -export interface ICodeBlockContextProviderService { +export interface IChatCodeBlockContextProviderService { readonly _serviceBrand: undefined; readonly providers: ICodeBlockActionContextProvider[]; registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable; diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts b/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts index fdfb906e5f1..8c790c54040 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ICodeBlockActionContextProvider, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ICodeBlockActionContextProvider, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; -export class CodeBlockContextProviderService implements ICodeBlockContextProviderService { +export class ChatCodeBlockContextProviderService implements IChatCodeBlockContextProviderService { declare _serviceBrand: undefined; private readonly _providers = new Map(); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 62438452ee4..d19904b8d51 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -13,7 +13,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { IChatAccessibilityService, IChatWidgetService, ICodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidgetService, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserAction } from 'vs/workbench/contrib/chat/common/chatService'; @@ -96,7 +96,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatService private readonly _chatService: IChatService, - @ICodeBlockContextProviderService private readonly _codeBlockContextProviderService: ICodeBlockContextProviderService + @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService ) { super(); @@ -119,7 +119,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } - this._register(this._codeBlockContextProviderService.registerProvider({ + this._register(this._chatCodeBlockContextProviderService.registerProvider({ getCodeBlockContext: (editor) => { const chatWidget = this.chatWidget; if (!chatWidget) { From a60a4cebbfa2de57561cbffc029e82fe97fa305b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 11 Mar 2024 22:07:35 +0100 Subject: [PATCH 125/141] add more data to telemetry event (#207360) --- .../common/extensionGalleryService.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 1bec23931b3..4bf57028d9c 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -706,7 +706,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return isEngineValid(engine, productVersion.version, productVersion.date); } - private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { return false; } @@ -717,7 +717,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (compatible) { try { - const engine = await this.getEngine(rawGalleryExtensionVersion); + const engine = await this.getEngine(extension, rawGalleryExtensionVersion); if (!isEngineValid(engine, productVersion.version, productVersion.date)) { return false; } @@ -914,7 +914,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { + if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { @@ -1046,7 +1046,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } : extension.assets.download; const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; - const context = await this.getAsset(downloadAsset, headers ? { headers } : undefined); + const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); await this.fileService.writeFile(location, context.stream); log(new Date().getTime() - startTime); } @@ -1058,13 +1058,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); - const context = await this.getAsset(extension.assets.signature); + const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); await this.fileService.writeFile(location, context.stream); } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.readme) { - const context = await this.getAsset(extension.assets.readme, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, {}, token); const content = await asTextOrError(context); return content || ''; } @@ -1073,27 +1073,27 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.manifest) { - const context = await this.getAsset(extension.assets.manifest, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, {}, token); const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } return null; } - private async getManifestFromRawExtensionVersion(rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { + private async getManifestFromRawExtensionVersion(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { const manifestAsset = getVersionAsset(rawExtensionVersion, AssetType.Manifest); if (!manifestAsset) { throw new Error('Manifest was not found'); } const headers = { 'Accept-Encoding': 'gzip' }; - const context = await this.getAsset(manifestAsset, { headers }); + const context = await this.getAsset(extension, manifestAsset, AssetType.Manifest, { headers }); return await asJson(context); } async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise { const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0]; if (asset) { - const context = await this.getAsset(asset[1]); + const context = await this.getAsset(extension.identifier.id, asset[1], asset[0]); const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } @@ -1102,7 +1102,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.changelog) { - const context = await this.getAsset(extension.assets.changelog, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, {}, token); const content = await asTextOrError(context); return content || ''; } @@ -1133,7 +1133,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const validVersions: IRawGalleryExtensionVersion[] = []; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { - if (await this.isValidVersion(version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { + if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { validVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } @@ -1151,7 +1151,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return result; } - private async getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise { + private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise { const commonHeaders = await this.commonHeadersPromise; const baseOptions = { type: 'GET' }; const headers = { ...commonHeaders, ...(options.headers || {}) }; @@ -1177,24 +1177,26 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi type GalleryServiceCDNFallbackClassification = { owner: 'sandy081'; comment: 'Fallback request information when the primary asset request to CDN fails'; - url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset url that failed' }; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; + assetType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset that failed' }; message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error message' }; }; type GalleryServiceCDNFallbackEvent = { - url: string; + extension: string; + assetType: string; message: string; }; - this.telemetryService.publicLog2('galleryService:cdnFallback', { url, message }); + this.telemetryService.publicLog2('galleryService:cdnFallback', { extension, assetType, message }); const fallbackOptions = { ...options, url: fallbackUrl }; return this.requestService.request(fallbackOptions, token); } } - private async getEngine(rawExtensionVersion: IRawGalleryExtensionVersion): Promise { + private async getEngine(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion): Promise { let engine = getEngine(rawExtensionVersion); if (!engine) { - const manifest = await this.getManifestFromRawExtensionVersion(rawExtensionVersion, CancellationToken.None); + const manifest = await this.getManifestFromRawExtensionVersion(extension, rawExtensionVersion, CancellationToken.None); if (!manifest) { throw new Error('Manifest was not found'); } From 613a0755a833432d072ed86a606bf239d138642d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 11 Mar 2024 14:07:44 -0700 Subject: [PATCH 126/141] add voice stopped signal --- .../browser/accessibilitySignalService.ts | 8 ++++++++ .../browser/media/voiceRecordingStarted.mp3 | Bin 30592 -> 27712 bytes .../browser/media/voiceRecordingStopped.mp3 | Bin 0 -> 27712 bytes .../browser/accessibilityConfiguration.ts | 11 +++++++++++ .../contrib/speech/browser/speechService.ts | 1 + 5 files changed, 20 insertions(+) create mode 100644 src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 0468f46d5d5..f53aba96969 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -315,6 +315,7 @@ export class Sound { public static readonly save = Sound.register({ fileName: 'save.mp3' }); public static readonly format = Sound.register({ fileName: 'format.mp3' }); public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); + public static readonly voiceRecordingStopped = Sound.register({ fileName: 'voiceRecordingStopped.mp3' }); private constructor(public readonly fileName: string) { } } @@ -590,6 +591,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.voiceRecordingStarted' }); + public static readonly voiceRecordingStopped = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStopped', 'Voice Recording Stopped'), + sound: Sound.voiceRecordingStopped, + legacySoundSettingsKey: 'audioCues.voiceRecordingStopped', + settingsKey: 'accessibility.signals.voiceRecordingStopped' + }); + private constructor( public readonly sound: SoundSource, public readonly name: string, diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 index 3bfbced34a36c38dc0e173ea5def13e0aa219867..488754fdd584a4c331f562d37d04488952bfd766 100644 GIT binary patch delta 6577 zcma)gcT`hL*Zw3Yga9Fg9-4$I2+9df1e8!jsvt$Bcnw8FK}A4BEawnfLQ#4Z3`K0H zAcBBi2}M-8fY`5~ND)O8^#UsAi}(KC_pjf#*0d!9XK&t7ZxJZH|HK|kU6PjQ)+ zUWO)SbojUY7ucJw|F`GAfC2wMpZ@^k4Ys!b`1}{Z|9SNrWM%4WnVD?z z!|=Zy|Bkgbt{YdCs)unXt`c=3BZdmW1D8Dm8~Q5hj0%mrMH8Z|Pt%r7vzddz^-A0} z>mWH<@q}e0kN@H2_33aO~vhnpGB0;Gtx_l^Q%c_v5V>Rcm9~dLjYSScx>YGm8 zgeNRACvNP1mJ$#ktIpQ9-}&U#BBGN}J7JXE)c^cPtJ19+xX$fd#3jdMRb=^+PP$d* zM4*qsg#oW@PG_KxQo-=lVnDLW&ov$%9sw=6|4RPasaFAx0&zQJiIL%s)*85ja*7s^ zq@|^m4*dlHQ5C~bTp19E>KFh4Q&=mX{8L~-0!6mMFg|%vU0coTQ z-~N0%u0u>_>2J~8!T{`YHm75WVy~4AEuZ+7!<3RmR%O{u%k~1XA)QiuGm3N6Nw?R^ zYCgFMNu~w7Za=T3rLA=7_w)r4_}9r%ncrFYzA!M~LAF@#Aj4DR$uvQ_cJep=o;!vu z0tEp8xD`QSRv|xrf&sq3v=XQe3=>$Wz(P=fJ0oa+OO6gsAAtz^v9*RCwhho4*XOmc zagsJ9Z}XIDyhHdb1bz~0VRLIUi1a}-0#-x=AcE$qL0AIa2jPA~V=xs6fDA74IpD7W zC&+lF5O~Rlwnm7vQF#)vn4_Z2dd5Brkl+F#~%gIK{bwZ*Vii%a9_{EeOxAfXKdO5b?{=$7K)aGoR?2zPoDYIhjEoKSw zTQeVZxfX~{c49aS2cXQ0fqL{-tIOtK0Ipg-W?S!_nmWeJvvNu9?CT z9=9b#v%Y1Rxp2w!NQvR#NP*|Bu#?7*EWfn~mpYcWzW?)iaai2&)?-r+6l~cTIeIOv z6|2Nn1R?(s0SD|5n4cg8*Ts)Ae27HLri2TO*`$8>9_}X`ic5s4{0lIVDe4S?L=Xjd zfXRbwP<)3-yE>ql9bq>_UR{-N03SP(K{=G6=ri`(_ui&`s@t!551uMKpm#b7Z#isJ z{cusc0D^bs+6N9SZdcL@YF+wBq)`Lyw@3_& zpFiBs+EN_q^~{PeG!`Nz#p(6IL_30Oj$#{mWSV9d7_}ERCQH7X1CLIN%8vY1b5RfXN3?rK(lZ$uSTeG&O(y4p51+CMm?BgtZRz1Os&;P{}H6s0jGmrcY% zCxtX94IN~g8b|??^tErVUX&dN}+e=|GIj0)R`!wvL|wUl|`q#-}kSt zPBxqg9+#6Y4|Pj?)*`g=(2Kd7vnvyeJ$XGz~U|iFrEJKfd0T-~ZOD&R2Q-;@6hq{EZTJ*Sco*syDq2$sa&gcJ&}Xe;!*3I)3tM zlz!dSu(eZX^lM}zkL`don4QcDjkYMN*d2&yAD`$fxk{eFSb5W%#cHwoTQ}RZ5<8M@ z4uEnYNdLW$`~ie+FdWYu|NU_j=f?#9JvDFGjdg%8*hd5pSlv$N*B(vJ;yh>%eXw=i z@O;Nv5<OX?# zl)iSVatRE|HqY+y#Uw7*dnURFd5JnS z<_BZyMBYqAoa9`-f}&-O-OSF6b-4>Ri-?cq_~h0+PAQZu6)2IorFiqbdv?}!71im9 z4#?LGS>s@D)s;s2gUbLG0mE=&5-{ZU8=Ic*%77CdmY7Z^X);7`+u$JPHCPKMz>>@| zbzCop7=+{F>v1$f4k!driB1n{s7?-h8{^QGDVAp{x`k8y`3x+&nMm_My1b;?@^jmu zZR}ToY2aOj?s=@Y(>qEzY^$sGnC^*fUCAM3I-9f%j)refzHkGFR^6j>v*~1~T21XD z(jMz=WiG>H&^mED{j`a={>KQ-REhisTSl>OuLeu;pEXYQkngl zHo6qvSd}Cq?Opd^s-*x!mN9GrGw>^}**;RLc@wMUW)8+2#cl)}t2h8MjWVW36IYj^ zebBWjJD}4~^9U(Zl5t5abg{lplV#;4p$B1EU=PdlO<+}2-LhW&7sTPSA4I#_9Y3Pk z+bo&gX2aVT^v5nP+DkL%jQTb|_&8bv zn=+5UD$ER+g0k@O6qXP$MU4OrR65w6AJgX^3~!P<@kGmc#(MIEb>Q2+*vDTQ? z)dzg@eT&)y;}p|2xPK%+7QcUZ^pfS1b34|Z@>*eLZ=mjsHb0*C>MT-a&6=NwFIN>ZRc2Hyr$1md2@GtY5f5n70tqvI=7pJR7P%Ll|{Y1sS_veSu~U88nAJi+GXLPiIQ*S&)@o-EbAtKQ4aC7*%suy4ZI&`U!JeAvc^XN2LF)F}9o1fSu^ zPK9IQePIn$SQeLw@WXL~d=I=1-yg{E@nG{cnxITGnUGPgJ=j!(9$>BmBjB*keqLM9 zCJR!WCi#7+VOUhn{(D0aX|jo54;naWU-LFKD7dfBmLvK0N6csH8w|Or z>-J~w%Lz%<9@^T2efQIxbT&`btsf0xr@#8pitTyTum9fi#jN$eMFz3yXgjo|_n&o@ zZ%7f9T|xybtkf>hj<>NNh}Wl^_J=Um?fN&Zzt}$)FA5eg1%dF-Zqkicq{<;u=>VT$ zzSlCx3Ev~?5soX$|i30 z@hN+gc?!7uC*sqhJn2f$C#`U&gUQo00#h_T2T>~`y%3G(0baN%$T(@6`r zDb>ye(qhPcwcEc=^4fL<8;V-s;sD?Msy+?e>^2O_uIU!?C0bB~XV#(J>`);Lj59K^Z(cn06NrB&8oRMW8%> zj?HV^=UQE68PXBW3F8u+P;u!AS!u8Q{f!PS(P^0;UqZMSdoLo}Oq8`MrB^$qXPX$% zk3DL9aih1-BW9GX?;Y%M-a9+w)(_7u2SPPW0!%z#*UUM+d4*klzHaU7%70EY_|xEVmnp$WM1E z)i!h(xf4HS|Kebek%{9AUs9cSmQ&t#$Kux?T~!)*>5mg1yPc|U)=N9Oax1jy7BYME zHuh@HT?MiWuugd|a{V}CKz97~jz>dh80T#cr)d}R9>|1?7^Z0A_OGBuxQXs1gL@Jg^GB7AXP$N)G? z!NX*O`6(tp3G5`lu8l|dd?kRAL9I|C9>Ry>Gsz&DjN{=umZ1Q?0!%1^5)-#n77}mn z)F*h1iNXv?#X!4$k*E6rXL=DkWOmm z+|X?ii+eXzuH)%)PBTNiJ1B9cNYW#$B9-8>&=Oiekqkt>497%O>yD=8_>`2KGpN5^ zXMSXl#}~hb@bk~&gr$9&7h?X*jH-v>SDXXLBCmQO*Z90eT zV+(3;-jv0o5VTXuk$(QB%MxLxITgE^Mves6r>h-dK7CxOKrP$jEuD9P^oO_lp~D`Yk6pV|r7{=Suzap~ zxtpWjosm{Q)OW-j!17=s%7CT#EwJLfO^g=)931O@$(S$xg+WblhSynp!32H+EX|ZR zMWeFp*Q>$o$A8f9?&`=;bxz;e1_k}Mq+RZ&CVTDeRu_5Oz1!C=pXX5W(ku6y=i!vj zs!gJWa{C|4PLV_)SvKYjNkVB(Pa2hfMjWRm|QJ-L$)qpVXI+E`abX7 zZ{FoBhK0A$+dD>?QLj4tr@b2627X+w{5*hF{*6sz7>|Rm1iv|y3J6yEcc33Otdw^J zC=%*I<=puoF(bm3lrN3{r3O0?HSh$RVIob{79k{7LJr}w3*g1ecJiM2Izh&p9};dhS&^iABOQKknu{&AQUK zDx&(0$y(p5qfLjdP?B6IFYBnA%&AJk{FyJzNY^|)RaNZA8hOL?molAqY>s67^lItt z!Tq6zJ#o1)i#-`>#g_T7CcYlQ4>~cNHhC7}PBO}+7wVTr7)EmSB>pMbL}&p&Xe0!c zqYs46s(=Lr^Ao2Uwao<==u4gwhvX^+s4>t zp(Zkn9kCd8?pxsvmrvtzRbC8C-a$9lTx-j(h|z85k)I~-kY9JtV8|gJKU1_<%v$H3 z&eB$v(ATa+xBh`C_s(A5UJcwLe?-1ljM*zgNrvch5nL1$v`LR1&k={!N68{mqSN_D zJoH?uRsM8~`t$w{?Ay7wi)`PWx)PcvZH3vSao&jgyS>f!S9p9%9c88607c_77%1)>(?;;D?MCFm)CG;Ea@N7XFeO{mJ`+-ZpBub*KjzZIje8j zw`$G&|3`2CzYb6CSq!XWbbF!MwZdCL^4ov{{U!Uf(Ep`daI|1&Qwj8ZfSZ@Qm2lmf zzX?A&e`MMWKc@K?h1HJw6H3~8Y3>5Qvuc{`4%viPVtHPEI@x06?Gn-Jytjp4X0(!|lci?$w1=wiDLJ7sUcme)7DL0RXk4e}Owpk{r&DFVDgK4hG5Wh!D7+ V{Ks8{2yUTtLFL1*cXycA{~yJ@659X( literal 30592 zcmeHucUV(Tx9+6U0)!rVK)QfCbPzNYsX+meCQ3k>G?Aj9C<(nP-B6S&f}ny5VnITY zB1KS8v7!_a8-gfB$PN1Y?(>}UoOAy4e#guF!Jb``^{#i`y=JYMU}2&T2h1jU#D4c~ z0Qdmukk7#bc$<(zcqcpoQq@q^G2UejfG_Y0@;PW99<+0poi)HiznZDef9j|cG=5)E zCm8+i5TJja`Bz6>(@6K%Ep-io)_-~iGKIh|Ycc<_w%gjq=wH?#==bAlgnv4QMmv6A z`HcoLR#R8|zZZUWG&Obpd6xOef4sH+ng8L+Iwu=#gMkeOHW=7oV1t1T1~wSjU|@rR z4F)zC*kE9Tfxm?T#>#c#FJH7NzYgfxUkC8&_d%5Y`?&FUgMt5&fpz`R*YEr<)sF;! z?3f7$g)Ya#i7RqG?ewMYtMiUjHYZ{o)l5*T4c}8@UF-~S)fdL&v;3E>x!Db8rBQS; zA8SI|PUWuJqfaAh%kIV1;+=@)saP(ySTeXVtV~uW4P-iq9-SwzGqchD2L}F2{h%gx zzCzG^Kr>q*JrpzcKYaZE`7N!Q@d00uaLIU6sP19{0f`-QD$4NJ3(bjAyPHTb5&{=tuxJ{&qs~_0(Oss)bV( z9_0f%^(aZ>=HWMWSTI_;aJoJ@3x*LdaKWF7^xxO`J)G|cgTa{J{-SHogAr!0ee;*Q zPh%b&OT_LNj<@*6;xwCL-)Y@Etp!p)KD%;Sw*)Ryi?*BzQ!atUqF`J|i=4~*w(S1G zW$HOlw)w@L`%H)-r1(v{V$vhKJN5`Cez3cw(03{1WMZB9`<1N+HXWBS4|v~|9yC`B z=~xzu%gIFycfap=ASxonB}Y{ycaGP|Q@vfaHI5MbXhOj)XT*j>3q>SDFFcOVbz>|) zdFglUXbWR-RI_;y(hM)GbR}4lZ|0vcX7$KIbDqK7#K9zRxT-}lCtHx5&ECex7xPA0 z-$~=ZKncKv;^W0njvC?L?X1SK3(2~Avt9^Gmv_oAqRO68QquwEUJ$M?~HtCwIyHN zz|i3FgU8v;$2YH49xa^-G?)2!L*MhT$!KQ5W3>q=Rwy0Qm#u&`v^Y&IkaH?hIj72F zq)|c2vzlz5t{vyQyj$Shcp5vNN6FpX6t@k2jeu`GeTxDVKT?GHxCXe|n|Vu2>&}aeG$7bZwqq5WSJo_o^U7*S=zzcWN^%tM609f`f@(Vd~Y%hKsapqU_a05l?QbNcVfv+BgW9^)8T^|M^gU>ZhA&mMCDzvh=Q2oYTI#;(uonTy9 zi)Jv^PBLCIm300_681#N&aVq2AI&Wz_i^c)_jxsoc@%Yz81Mb?GCi;~x78W7P;GwD zj|}N&Jc|D+MXtgQK1FOw;Q6k}Z=ut_(vY`C-xiQlJ7KN+%GQB5u5zHiLs4TTJ^F4- zQN?A?i;mZlToQMC$!eBilE;A(W$1{0{#B@obH^Z&%|jr=ep5i&s}?2_CZv==PHEi* z-50H2p;?oe;Nj|+8Ph)X1S}G7yz3z5t=N{dn~SDRM@F2o1!G`YZ+2Hc(?bp&b539ZvK;hY6KfkykK+ z*;8zR2+iI_?vn=HO^0}@T>Wn^gjZ`tkpXA8=UQljJznEiB zJI8gvulHU>G=uT;(^-6V{L@U4bD8h4BRF|PX`%~+TS4$ptJtU%G90|2AA2aQpCzn9 z8Y@Ng|E6)9flQhuL8DmZfHTi7uQy3CdhsEQgj zG<8WsaYz?c4v0K%a+ngm^F8c$amo9vbM|^2U8cuW)|urKE7yH&Rv_m zeL#MTu(+ypd1F=Cv*(q5(~4(95tp3eJaV`B8PnZ3zYRTYy?_cfuzS=sI_{U8?fQiY zL0$e$He0NsT347cE69hmu-9xN3e{4uk5IvOkRTXwgd$;KEK;(Oczn8Pj#$y*^IK~t zr5t~ju3Q86#}1;uSU1)6%Zh9I`G@kp4&5y$|6#X*S5U=KDBQ=kue-j%2rD?+Lyindha&3>$lcaJ!$ zy$?}6VNQwE_xP=^a=%3DfKQRZ8k3(BcS??z&`nNqQ)DaJ>Krg6QTjNs(F@wa)4B^{@0rTMq6iMcVY|7d&ZO@Yc_ z%j%itegt)sgHT}-ny@G57U2p!46-7=gM>i}gri48IuxV=+@3B3`1z$`-{)AtqwJnW z`YelGEuDE)nI8W9_*B*Tkj5>C@<)n>?`nN|*RA$NV(<$>&%5(VBxB{tkI=4ryIsnZ zMGqa8&N;5@_v7xzZen><@ziy%*cK)blHss0Q~=5}cy`zk0!{8gw4_QkV%{3zD$VGHY*&)ayHIU-p}%j znNSvs+o$-NbkVMFO}w`{?e0Y4bG?uI@2%8~*txEsT!17JZp1Df8%yy%G8=8@%5!P| z4EI}=HcjD|uvLrTDV7pZQv{7)4ve9u5H@frAS4tl0wO7<$R3Ix;H>P9x;9LPt_`Z_dH^(ezmC8KxUdEGDU^L9^rUaLk`hEu-cS$>DUhjE|M7j+_K-s1 zh3FY|?eH_h)M{?QA7L2o(4x2g;T##y+K!{r;GzKf7NI30^>v#ub`uxt}4O7<~u<&MN^DnTK+f985XJ1pr+aNSGfLK_sVi zziKUwtx4TovGTwq`^wh$Jc&ykW4HUp4H#J=p_5v62Xm*J7G=-nCl*hs#tNFhD-*|! zDijM*E`}oPX{R~Re&_R33Q8X^F*I7`%z3leSekC?Wku@|Srd40`_Oqj4?8&*x1$jm zyxy<~6l(`Ll%kqWrJ!mKI#wOgDgXX}1)+lTGvsVIc#OW2PEfe8Kk@U0nUg<$zPP>Z zNU!6`Lj|hi&m9vP6|cWP_^BbWX0i{puL8hzqm%0^>zto_pO@^8W-wMCo-jK`!r>?T zUN@%n$=Otr*zhO&wL&iSiH$jsQ0w|hqagVR`msw{V^LG`CDFTFv4>rkxmz!_apb$f zRyBjM+-ZP_Aps&U22%$*DC`h|!X*n>>1a3`j%D6(7DaC>^L7*-y6a(iy7aV30zQbq zEs<^2iwf23bF=33o~rRuapCT}c%Z!Ujb_+=!xHhCml-A}*kdB!+%9MGv))RFUq6m& z^~QedKXHP;BNc;ewLy7~7^B46A0mI!Z-XW}5v0+#z@}+@Z+lsQ*!}~_5wzPkcvEv= z0=VW)if2@4$BK{oUEg_@MjW-8Pn&#Dnz!32yP$#pgHxAcnJ=|`inI1Y-DCF(0o$g# z0aBE}0(sjh=U_Nhuj9qu(Cy4@VMRv&hZ9t0NYYeJ%uw?;q(csHxaM;p22~o`KzPbl z2uZ|2Jd`Xh)ImEMoQ$L~%Lf|Am?{khVA&RIA32izh*m=!gBHqn#qDtBJojwfB&;fT z%yHPK$>jM`HHTz$UTRk@wJ3!1_7ffZ{UqIEPSbMf6`a+PM-KC0n=TO@1lIMk-o6aK zyjfh)R?g--BB4*~R%ExKzlA}MmNhHd>k+K<4uHcToGBToHxzpql@;bm;cA(JHF3lP zJdZ^0b~Ej&y?ho*Ki-(1eeCf~O|xFLxU_})P{D1M6X&`ZYoD&Hww(EK=IF{wVuOGN zitCg!#(px}eNpK_&gUQzN}!6(*k*r6#%lC0BqJYY3~VWOcxsx&2Uzy3a4F?G?`RdT zeQA6E+q!?|mEr_|);N+QiW6``p*wMXk?l$W-Ek()J)t-o(R1u^tg$%2vsG%SV zC|dwPUJgXjcY>l4Sg?t-gzeohj-G_e5@Jo;6De;>`M8$&ly%90)0nrrWwV>TFP)CR zGvnGau*o%a*RU-o)f-Jd!`1OYkMq2Q{U1Q*!{PDqMJ8o3MR$;uQ z#D-Yd`B7%PM}51_=*=^DU2Xp(2N=@_8N*una}An&{=KYE_!U(LbYV97VTGWNGnqfn zsYn}%krN&g-FEFYW4*RwOouP^&)YwSE+f9EukIxfGUJ4yA|s;4Rql7t3qy8D8$?2a zL=lJ=lt3cP->ASTY)A^4#0<-NNH7bdMh^-P+V_?`&3wiAIjGaqx9Op5vK^~QhWvm~ z)3VY*-a|Q~)fplJg<1!Dxl0Jo5{dgs`5NdDzv?G;Z>SbWD&-E*q8U%tk*r<5!O*U4 zd&ciN$bCwzU4c)gEgintyXeQ?u@6?)iif3PSPxSI;H5Em7?pwqNojCu2_g+mloG^Y z1y~Lr3jrEwoaG+^Q)6H}hkTErKRc*>s-Z=$9g$pdJ$Ah0L`(G7(JeZM$|TEbxJst# zV_Gq2#ITF=Jys{uezK%#-}8Tz^x@mF<@BR3En<~~BhO6SVLPYCy{@0r6r`y`YwTQh zaLUY6{E7J>?ALwl-1n{9HrJFIE?GBKC#1ovOV`eoCh()D>vp(<1RVRACK~Q%4hVR} z8#)gSf!}%l>D~lvhQL+8spAY@!RNfP^t~*kRl_u;%lX(vSf7UUM`q3S7L^e8*^BGn zUBbnidc5&V91Lcw8s=sl8FrpVHTpQM2t>84GsbKe23i)1=QMFU-k|bdC+6ml;SbOb z)8FAL0!g?i3YsQ2CQ0J~+&FIhE}A%i(cna+VlWvog-C**U*h;|n&_lVe;4<2=a{OT zuZ%I0MO{8sswy`_DTh`4dRXpf536sxo@u-c*R9+&k61Q}eJpO65aTt*r!kMT&(Dxj zyp^ZP;_ut?3ku`)jbg_3_aJQxk3-tWiv<||A*D2AFdkFbRYkJRkRfpqMbc0ZKM6+U zAn}%g5`8-VraY2MsrOhH)j~DppvkqjHrBKG&6()kUyVq9vR#zlnn*)GCBghg5Xohme z_&hRMptF3(HzeGU6`UceB6dEimW8*0lvWclpJYQJB`zBpMt$IVm(Gxt*!9qw$>N1C zU#>(4=`R*$UR~r1yVgvQy#HW``TZ$ByKIwlBoB8Bsla-FiVr1UWUoKFYq>tLik*j| z%uG_+z#EDJqQ}X|6xe{t2M{-aLtE)wa)7|p4>^T`6mT@*_~IYD7tK0;EZtRL$6i+M zx9P1#)d$jNG$YpLczHtho0}1+-Jt`y@|l6f#>wXi#>4^V&V5a%9`-G04eGwsRvefq zO446_wDae?=8K;FsSb)99Ex5!_TI*zhq_rkhx@8G-q$cU(_D0LtF<%t>X&21SD3F> z%at3(XEi1cdy3s=6$HQ>y8f8fI6zabvjcmH~t`$XZT(HVzD7qOblmER%Q@}D9D_s zO;O0;g!5AnE1d(91+Itljf#~O7mq*8D3`&v)V{U4>Gq_|=|w|`+Vhj&CtrQ8@_Mn_ z#hB0Pjs9W$`byEKir!&bgiA&Od$#c%1SG9Uz%8U;Md^MsoUM7~L01?p>v-JU&VHB| zpTADCiM7oazPX>~gV;seRo>P(X7nQAQOE-ak+X5wF^CCD0>`o*e@X!|(!m6=SQwni-W?px7sPA07t4J=u+4h2 z2kfVHQwbMMlE@JU!2WrFfyKg$;5jAe5(6YzqcsLgIqt5#xw4J6D_dizZF= z@`uZolD;38m#CjJm~*$c^uN5_Ph?_`6@*k!m9ybWL%X;{Mm(f$W4#*r^dR=Mtv_S! zHIqih!1BJ6`+ItEq_GAh4?!(XQI4tjLk9J4zdf5bBsTQVnXJNDIjwJB@JMd zj{|sg66kP{uA1mj40m7nt|e{b)O#w`%5zw))zIbI;SaIXE%MS&KG&XGTYC^$({I&s zYyX;F<@M%HZAc9ItjudaiTsX1jHc}-4TX(c-rD3nGe9l*pXbqZ($lJ zR>LkGVVefzts%+ej7g3PqKd6e&w;U{0$)QT<&Mi2T=n#rTe0`vTkZM$fW7;j4wkGP zgRBuzQ~k2x=#N3?ox&Y!pD><$T8Hxd($bf?CnasNe7JiNS-9>=KHt`p=x=>Dd}>== zVHo-$W{QXb0d*`9IFJpP#YH@Xq@y8rVrHcAEsMCOp3^0tQ^o}3^V%2t69+r4Dn1(C@in}crPlGNk-{MmR%*5>ZI*#RL z?vux@z0nzNXos52b^W9P=KR%xm`@E(y!bTZq)HHXR68&CXR8?5-}*^#9Slcvn+r}E z)o6CIC~$Zq0GxWK4- zUYcY{VA_JFJ0~Bx7PHv~>RXw*5)1cdMaj^efec*-A;S&;667X{!7V5i$W2=Nm7+2Z znxQ)_i3w-Ews
    6qIKg@@+pkFGsG#hBjg_T>K3@+pI2M)cX6^LI`SZ|k|9=gB!C zkq-?UirnwMIevr`0`JPM-CjN8xK0z3t(r$are2jmFXAFKko$Qw{SXZ3Jbh8^_9j~t z$Pt4n5MgM56|87gkPnEWD*+t63EV48sIy0GL+c#P21*$LSO+`!un}5XO&h~%glozZ z&Ek(s&5E1T74Lr2(EHdq^H7G0Cpp^}K_0jCq^Pj2Ib6M8GH&)!rn8_odOZzmVXQ9i zkN$b%J#+=Rs&-^jTs;T*74mPb(>S{K01D|sW}#dpvjB5@geL}lGfX5y0i_P>W7)$d zJUbs3CoH358M6@IoORzeqlR;_`?@5bSf+=}-m3lE?34uO9t}s4=X`7nw&XmUh~=#~X8zx$B=nwIqlOZ{0u6$8NSCN52p?vagx~06 zAV&$bsnGz14aZBAZ7Q8mN{o;wSKb~^;fRpUAf6P3oyqeR#%Ah_mt6NsHB**Ouo-`& z>J-&8YG?a3EH7Bfbhyn)XCfr8?lC6YUgDw4>E9q2Bg3x;Bu4S>VM*7$=sHdZgA+B_ zMPwcwaDCCP4D+qU*x`3z;K5pbm^#GZbU7J|Ah8Cs?>CZ=2wsbKisG~pCEhGEv=lNi z-}c!gj`;Kgm#xk1Q|-$=e5Ca8ikEXM4wFH~O)um{GCZj7`C!`c1{MAMeTJ_EZZ>#i z%k00#Trgb+vex)+`Oeh+&5-Lo6N0{fAED#k3}irGP?PvaKk^Q3vCR4_QR%78$=yNN z)xBuu{MEX~)X$HGCby5!L%J+8Cm=D`tK=aTiXjVPk*R$4crUjRhrUuI>2M+VcDAQwgMak{< z=g;wAXVcvoW;zVU%sSTZUxtsj*!mBWg;0H-yp0har7UUWj@V;9d+`+VJL-M14!BOn zQas35Fhe$^;ITk&S_cKlc?WWkjV4^JdU%wOg7Wd~xc=JCZQVYDv9R3Ie*zEnQVwf!*|};F z;2BUw>yX;%%68~A(BXya*TQE3mmDh{D02=m>n=G6q8!gj;KJ(o5x<=>bmg3an@Bv= z*Gm9fh(20|~EVXHT`uZ+UL~7aI%ygRiMPr?0&QExZ)K8H`?Kr#4+%^Fg z@YIu~SCX-y%~)+&ZN9bbc==Xx1@CM zHqVh;(&u|F^ySQCpUzL-tS~L>HDct#dgq*vwx34?(&SdGjMjj}dIV&)?b?Z?S=*9KZKF_eJ9r&3^_cLz(rc=Yed^@dgL z4@bS@UJW>vm{QvP#|T=n(F{$o#8Twv{Ysn`-hfI~mO$X>f`~r)Cx8zIjr=g2p45+* z7ca{}Bu#EB<0K=xTJ-qXY?n-6@F+h5ntkufsFFYM3E#(uR=10E;K9}n3;?ro>lY2rQVS4|MmH3};s z`y2<9N-2Ug=!TFqMG4}hXG5|SDGnfZ(Hr0qXe1dEFib@knIVRKNYrDT(2#i^8XwP@ zBK3{A3Z$B3oqSSY7=izTwA>)4fPvmv(;tFFUBuWxtX`>;HoVYEjvMdAZ8d?WZ z{`h6ohf9suSSL8kwWs5zVHHtoR_Ck1k#o=d>-f z>rV7ql0snO@;MD`SQ}1%L5sVM491Oh7(ar}INFYLys(Ics$Y)p&e=ZBnt{$1`zQYB z?`c`D-{aYtDY;q@kCekQvwm+KKl8eERTJuDA7?Q^^1*@;2tbC_5xGk#cjEy19tI4e z`q*s5MBQ(R@R&=MmsFz0RCT4PZ8r8M-CqSUs;+twKFJ=ZRJD6iNW=dV63(1p3(~}xBZc*V63M(A zPL!VCsk3$f&Ck9Yte41fh~0q9k_~qSMllw_fzh|#etg}bNRweFFGJU&XdQsoOjl?VpxJ2=Yk$;k|=kM#~_C& zyl@L|3=Ak65|`AqE9hJxfL;#kdCJMZ7p@?5SO^bOM6#NfQ8TL0FM?weWC^x!{Ppeb zJ(y6mE8Ch{`sV7VkoPqgtsA_{(BXODlCUavY?Dzit{H{EM0Pv@q5l z^dDR9J+1(~N9@t?Bx!1$pk5)c8)^~sCL9PD;<|ovDabtz&9Th*lkE4Tj+y`PWV+08 zzYT0tR(M5a#-IC}ktIluNJWDfm zan$^w-{vFV+6%RR1}-sVvUf&zGuHG+Th>l~U-Pi8m+yW9pN6LxYQUc(R(iurjH#o; z67zm4zvItBc#HAb?a$Aue6Ebe=coWam5?qsH_5-;le`8}UA3@$%<%bw6O!64t-|0PgVmsha z0y)RoLbVk_gvZEt5HB?hTF!sY)RQ?RPB(@FGMR&VgscGs-OxwKtN=;Y2ECZndPqm0 z!kr#C`_!60tFybX-x}$3xluAP8xs-m#`m>KNGn^vsG7n3W@8p7@>yej?sNWCed>B0 zABKwU06+LlOnm>4&m)`a~&v(^zIDNX+~l1IG&WaoT`B zFb3y%Ohy9{GJ!JOlMtrgs>J5s#KgWoN}>yus2yMHL!7OJ`}IAHFy zYVqV}ZLEJ{(_R;Z1nY>icSV=E?zvL!&32OO4Ezc^OP8OKl?#J$NiIC4h znGC;g@p0~sXs*Cgk&k5S*qvk;%tR0-c7uXHlt_^xx z+wJ?ccQWO}T)yCM4EW6ZH#4%l9M__Mv@MRllT=q3p3qKxLwN-1-j?|y^^Gd~Cc7#( zQx96%wa;q~yD%2kVJsYGXr!&(Sxl0wipMFG;M<6v-J zG8c7@Y!B1~fQ7g*FoC2&qWz{>UwOm1?Io-MJc`>Y@|0j_(m7gVtP*$081XfviJP$< z2*@0jO73-~U;iN=>1iKYGMeMZucMlCdFSJjF9|+QUay$T-yhat{CFR^b~$);Pr&*K zW0;(jlHn=-JlG>J^#yqgyf4V_j21Z*)-A?Tq(cJ2M8koZcMuW?L&C(1;Yr|-6gMxD zN)n)7k13NYca7sFzi1MytqS+}7RY~5;HtAiL~}^Z_(Wz@|Gd$)7iq_N+E6a1s4N6~ z5htZd?~w1b{qPCRraHl;D~y_7Rr|_zv!9o)ru2!@JJMmeHOzC^wlvK;N78!y$)X^A z9Q0!63WF2)yqr$;W!fUV*bsyFGQ3th%CDiKn%2lr>qW^Dn=>TR*GH<@#$h@k4R#V zwDPrkL+pZSuES>tWrRz49HuvqXTF~|d$_&#Q+{fYr_fk!Pm&Qw(wgmM8NQyo8hMI3A8D_zF-h{5njYI5U0+ zW5$v9(FLC>%VEA-u1Oreoy`FZ<`K05>-srGLF(*jTF-wHQl8+M@rUcI!vuH3r8cR} zuCT?{rg5PiJI2{+k>zk3U}J;r1Q~oXL#UG@M_`O>L|#@~hT~z~7_9aC2Ph3PE%0 zh{_!a>75F4Vu}~~)TD%B3G84GWXW8O z69w!L2eF;GiMmAzXjlT~?X)^WAOrc7ON%GkP z@eP8hqg#h0B5U5CdyzT*9yLv|JV#fHnXy*=j8xq56&Cm}CLEbe9I+vPPyFe=pziBy z7k}Md7R~RsHD-=H2>(gmN_k5r024AdMN6M6xDLrW<<3ew+1>d9SI-J3tJUln*s=F@ zn&>uqWTUmgYTL+z#V$s#=~AIX*s)3ZK^1?6M$GHh9725Lk>}kmGQDz-sqZklcH2AZGo8Bhl!wXd73KJ9l4kds$G^X`-zk8-f`}%b(0_cYUp&TYtk; zdu!cyiC@3KPMnYgKJDgY*aAGP=Iyrfs!So*0ZJ@$Jx{ zFN&oh=|AZHnv*b^Xu*oKx;b>=j-cZCSzD@V6xJll>J2hiByj&s!-J*uOI$EF>?s zr}q__OzHp5DF0q;zx4xfup87j0KmtzX7%v^=pbLADFC1;h4i|AZ{Lxw>X`I@Z-{?2 zx_|V;1=FUC0l?hRniXUED+7N8r!e=xDSPR7=IOt2+q3827~KB{(Eia65-^Kmrn?lu zStRp-g}(w6W;gE z+R)D*BKj{iv!S2AbYTCGTO0cMLqz|jW;XQmmk#V7a%)3Be~9S6)Xau{{?dW{LvC&8 t=MNG6mzvqo&tE#Qf5@#3{rn-K|57s>`uR%-_7AzWp`SlQ^j~V`{{R?qE$jdQ diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0532cf6b15a44c849ebf6270d23525910e15ed1e GIT binary patch literal 27712 zcmeI3cT^KypU3ZHk`N$3Ae4ZB0Re$WU}y@65_(svQbYns^+7?gE1A%n8UYay6%~=9 zQtYS?LlFx+QbYtE3yOkbL&b)`Y*3%~?Ah~f{@Jr<-`&kU%-oy#-sJw~GvB%S-O0h( z&X@rBW$tDlZ*KtTz-@hGbQsfP!+NG4(-6@&(KoYQ?hZf{2wxW&?Xzj!vgKaxfQif& zLkFY((6Du|`FZ3Y4MW82SD#-E6SGA>`~0JU{QExO$7o4^kLK;}Ve@M={(wI}$;j|m z!_3tF=aC=LAc(cS;je)H?rUOg@~iLPM`pYJ<4pn@vrL3VNI*zHNI*zHNI*zHNI*zH zNI*zHNI*zHNZ|hp0sQ+p){k>pg#M_=<3B3!=w~$t`&kS3LIVGkz^s3;Sw-ldf67OA zP%r_Ze+2Ups)7j!{Uex2HrJ^-!?L$t|(xNmNg+w#kjk~iJ zvfdp3viE_d-?QUcZqH!}*Qc(O!pw|1g}?&_6*E?QpN0AxG-}7mOMB@$FrO}de`S+e zx>||pjrexXlW&~3O6Sl=oHWM9Pe-0OGjbd>^9w${mCem)tW|}JXRP*R zuK8o?DCTGHRLMxrc-Qu(H%&P$?!L+mJv{ySG(XJjZ8MCJ&OT16MyMg9z$>uxl9$Uo zncFVgcYgn;{!zKi8jRZvGKa58@fjif`oC0QLK(zE z(wnxx3!=JFLqbv;yx61~GaBdS!O$fFh8VIEU_z7_4JEJ;7i<#@wi0R)F${)*6O710 zCL|CHG3P_*b`WLbLBaqH)nrPDmDb07w*4SpMgEwVe94>;7b|vWq8&D^elPd!Yp}_1tgDwPLbxAqo}Dw-Vof;f=B1q&&pF2_p00jl zMOndq1>;6*4gHSS*{7ZLoVE6gczRmp?{07Rom>=<8~lkmW)eK%w!5RC=IwdSnO7_E zmA%OoA8>DrBFo<-qSADeo*Y%KA8vBhl3w;cQ)bSysIOWg9Y+u;_?e08X zvVF@4G082|Hls-1;=IPWipz&4Mb$P(t_g~9j&U}ru*k}sDqFWYpr*T_Hy5uI@jQkf zTeEJ>dCsOvgD(M%lYf+{I3AcR>TRF7XEdqhTy~!0Y;s^GH`<^>xptgtHu1D`ewciY zA(z}CgL%2hl)AW)uz6H6O*DrvE}jDevdxg(u}scQR78JqptG#Vp_X%3$|T@uyIhBb z%L@lD#JdOl_2Wg&}q~+U>=B<^? z=4CbfY1L^ecLneCoVMbRkK?5+0Z@Mnk!bg+nBsTy%p69GizuZDBl=wCK;eBK8L?4fwdDLnmw`45M{gj2i zjqWQG+_HDYJA8@b#E%GPP-BbbR`6V!s)x~LdL68qqta!xLMaB8DqDylUMww|8&w9( z2O0oH2&`!sOo_#vP0AT5)b+zFn(E+93H7jhMQ$ ze0y>jy=Y*NNR|70IU_N_ow?aa<&Ux<9QLZFwnCOO@q`rqE#`DvF5$CKd3kSeO-| zWWmr0hy`FpF4uwJ#}EN3Vm|&)5E6!HphLQP5+|e$Mb$2F&k!NC<&}p^`&&oc%g<&Q z+G`mUko)(vj+hfgf9KRkC>u5TSf^0W9Py$yJk0ayxL|+#d5gLz=b==P4C{{x& zzEkae>1)!Fn7ztp4*#C)T0>1?XRtFwMyMdkPDBf0MzOU>WHQr^CKi){vEUA)jN4@1 zUT#vZd~$U;AxtN${LE{L^u46>{-ltVVsJ?F{Io-XoTq7Bc`ckNPoo9F z_+3|dei$KKHk+}5T$fS{mBgtlqGrYngd5Er(qMq3!df*3427U^vW2Q>Gy)iss3OXf z12Vd7n)QTHsw}9}tB=zjkCXj#%FSVW(?H;ZJ1RHJtRL`Pp2tnKKD%xzS9xQ`TywvZ zf$fKksj3a5&?Ftr?eR+}U+mKymj5Pgv0|RwXnN38o13V=EyhMhD8+dq49Gv8}T5+k$cI#1VX|3Y|xCbI4OsH?sZC<y%K-}Yu?CUg9SJp;)-Mj{RPUMK%{5)Zm}z4Lg!g<61`s(eYt z!847))viYzLz>ar3_260m_Dq!naq5jDc&kDcmNZB1(+r9C?(i}eAqToTlFeojjo2> zi*}`m+mCE^UeBPxJl&hG_noynn-f<3&G)jS>%rH!(S7Jfq*Tkf$9mg#ZI*o&@apM1 zTQg5Od~o?6p?$aIrEJkDB}!6r-0BZ>5rs_Kr}6E*@%We5ZM>&x$oR~AN_JbxwdFYO znb=YBVt3>tuhpgpYG$myP8WH0>pyy*E-&Z!30C7_3wJrrHYrYN@X6lz5N}D^f_X%> z&tc8aiq2YFSl%VM#eSrhD8>!hP)VgGIpRYSIq=Y2DhU(k$*=*iWK&R^PCFKb4Ax{0 zQ4`2}psWBjipI?lGSMTc8P#y&N}Fh(8q?9a=UJRRnh|1T4zFsBk$j%3fu@wJ`#dKb zC`cJCIFEb1tm9^K!)2P*ZSBfBm0y)h?P$l(93pyH+>q+;YN&6~Jz>n5_0JVRZe3%Y zJZT&E%Rday#}v6q{xksQG@}5_Oem7>EGfK4-|u@7pDg0Hixh7+^1nwL4tL}{ZT#l2 zk418s?fpMcV1xK+)by?W6wFFE%Qmtioq@$y%khjUN$oJPmp!10*7FSQ68JnqYxaRwAvKp^yygxRbSaW}D zDDu45De~v2Z)nWsL)zq^xHll=uf9FxL}tPedsi|)A!9%(%@R%wB5D9}FHyP}#8NUB zT?ocWU?ISWTvQhLF$gFGY9Xu=QS~5;!qL#Yx6TJ;Fq7XmIihFh`dH z77g%79?&H(!q=P2T3@6vO+2+FleC;pVFSx6ql!y|^mAK3l3wu zh+VeR8=i|PSX%&4Foful@-&l=nB=ecm?hO}_(sE)2KtdHC=X)FBP4Ja@?c65=fP}a z1c9VIm{j;9e-){V8lqc8NW5JLv+CIu&Cfw&Bh0twP8LIJ&&s^2A3S%pynU&+_o|Do zin*>selezou+@d|`l`>HcCQUQdtl4n;k~bRw(D(C3M?3nt~|5cYdFGp^_{xY1ur>z z!(TrxxOlsFuyMCu`^NgJwRydl22LNgxDoX5Q214jYIwo>+XsW%3Z7pcT)T39<<3aF zlB&Kbc%}@=e2jOsqOi1Y31+@NuN#)WwjNz{--QcHOyr^FM;!WK@k{1RGAN^3@|Gu| z;!t`6^@S6}e3+WfU-`2j)*y3$x#-v}qIxL2Lc{u#;)mw2sO)TK!lE_HDtiJg$R}Ow zGBX1_78K=ET~BPbEIxnwZby#SE)&HV%if?Um7th_yWR$GIDs{@i+KiI9|z_Jiy=0X2;x|QA0%a+nx&U?nP2VD(#~;xmgjhadVYV(4mZ0-BJdL zKOto=lcIURrG{}f>nT8+TnckhNx*~vjSQGD#)g0r8L%O?imYjrw5l8GRol&_(WY+M z8X76q_B!~TiHh+z+j;Z4g1%CLW7mL|vxXz-UnRue0m-8QP=S^}DHyz3dTpqPJPVnjH6i$b!>r%|bI99I97b39eMI zHwdU&5?&q9AzAb3Sh4GLL3u>5!^O(A-%rN`Jk76rKpO~{|{2^7*gGv<}GVh?F3p+%4+Bu{yIE5ur z85KuRL&g3)AbVMxIP_P&Q{KGZEbWsGns-zDXw`d*M;cO3k93!BI$0C#{3+wF>sODS zG`iwvIgoU;tm0wpbbg`kv&Z-ZLzC^pnA?OhKHl^`n8~YGS{z=>7zz2tuwbh&6j`Z- zTCBuEdFE<{M5!1B$hmu9WNVCeyWPg0?n#xa+Z)2a&>0EdmT#isvIfL1eLKS*lfM|~ zkTASrMS$0K=Q&$17D(n8T{vVpJMb~xh5Ijl_F`A(?c#h(I`bHR!IA-#CS2O@hA*}M z?iE%?tCv_;69;a^EAu2kwa*W&oWt#cU+g; z#;5sd*UDMfdGLUP)Yq&wi;9T0Lxbc9b>50J@vo{{q*`hl!AVpLiW5nLFhV+mJE@!; zE1^l_DU%3^MLRyvBbYBeeZ0S2NuwY}Z*}L)we{xnJeH_t&iW?`C6Wv*lfN3V(G#OQ z4%v-Hx%67)ef_Vqn|(R_lq{OkW9VKw4|}4-2A$!78bV$ zU-5?+D7tI^>}b6408TFUtnw@F{91P0S010K=HS)%%yGgRXqaF^g4J57-qs!)VakJf z#;}LDE#yPa13Y*b?1YWjis@#XybRUso~&gA5b_dIMa%nJLs(vYd1rNm8i3l047KSv~VZ_{4o_Lt+^@zWRn=Dh!E_3rJU6BSv^KqHO>EF@Mxf$&okSl=!2MrshsAf7w?)YeSj zE2V08tg3kFT4RN<>H)Uq*FOUr_k5e2;k?yitXd_1^rk-ny|gV|dME!%;~!ftU-4;H zj@p9L4>f7O!x!@x(|2C-$K&9e^F$F1WY#~MP`FtCa&o^7d!zk*WCz8KdNuKp6#JI- zfxUk5dDO}1H#1zn_Oq5;2lY{^Snd6f~Qg=UhG<0#!1~xU6|2*L2aLaM8W(1OyGa2OElbf`V z5hM9a8{p+TJTpoS_lk+WAo$nmbss<0%|!>x*g7(?C&Z;6gyn%SS{e4qN>xRvKpLgQ zRhHkKsi^7h;(6%a%;ZcP59Y|qHpxnQ+EJ;HU1)T#UIw8k^gvmYwVO2SxhxCAWF52) z+4Q421v=YVvw6e+nlkI3DgeDvx|m#Jjx9O9Vtmu68KGAFkwmQxx`G(kbb;dWuY58= zAv@7Gx=;F2&(!|G+PpvRl}%?|hg>&S_t`wD!EE+XlCb=D5}5T527oiXDB0YJAj*sw zkJJII4zWifSD6$=ZXot01^VwC|G)a#61JYh{P>0Ce~7>@|A>K$=n(%|2uprknIDNV z;nA#XU^Eb8^6P)$vSrJ^aB%+*Li^<(BHuqu{^_Ff<3xT%BRt~gTz~)qF&h7+um}nK zCj@@^NBI92{|O)AVZj81{t?Vas0t<^^p9XZLRByUp??JP5vqa-2>m0Nk5Cm%K Date: Mon, 11 Mar 2024 15:57:39 -0700 Subject: [PATCH 127/141] recursively group children for very large lists (#207364) --- .../notebookVariablesDataSource.ts | 19 +++++++++++-------- .../notebookVariablesDataSource.test.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts index b9cdadad7ff..18172e308d1 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts @@ -82,13 +82,16 @@ export class NotebookVariableDataSource implements IAsyncDataSource variablePageSize) { - // TODO: improve handling of large number of children - const indexedChildCountLimit = 100000; - const limit = Math.min(parent.indexedChildrenCount, indexedChildCountLimit); - for (let start = 0; start < limit; start += variablePageSize) { - let end = start + variablePageSize; - if (end > limit) { - end = limit; + + const nestedPageSize = Math.floor(Math.max(parent.indexedChildrenCount / variablePageSize, 100)); + + const indexedChildCountLimit = 1_000_000; + let start = parent.indexStart ?? 0; + const last = start + Math.min(parent.indexedChildrenCount, indexedChildCountLimit); + for (; start < last; start += nestedPageSize) { + let end = start + nestedPageSize; + if (end > last) { + end = last; } childNodes.push({ @@ -108,7 +111,7 @@ export class NotebookVariableDataSource implements IAsyncDataSource { assert.equal(variables[0].extHostId, parent.extHostId, 'ExtHostId should match the parent since we will use it to get the real children'); }); + test('Get children for very large list', async () => { + const parent = { kind: 'variable', notebook: notebookModel, id: '1', extHostId: 1, name: 'list', value: '[...]', hasNamedChildren: false, indexedChildrenCount: 1_000_000 } as INotebookVariableElement; + results = []; + + const groups = await dataSource.getChildren(parent); + const children = await dataSource.getChildren(groups[99]); + + assert(children.length === 100, 'We should have a full page of child groups'); + assert(!provideVariablesCalled, 'provideVariables should not be called'); + assert.equal(children[0].extHostId, parent.extHostId, 'ExtHostId should match the parent since we will use it to get the real children'); + }); + test('Cancel while enumerating through children', async () => { const parent = { kind: 'variable', notebook: notebookModel, id: '1', extHostId: 1, name: 'list', value: '[...]', hasNamedChildren: false, indexedChildrenCount: 10 } as INotebookVariableElement; results = [ From 968f1b2c40f5318098ce8346766b5de18c5c2717 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:10:24 -0700 Subject: [PATCH 128/141] Bump mio from 0.8.4 to 0.8.11 in /cli (#206845) --- updated-dependencies: - dependency-name: mio dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cli/Cargo.lock | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 80f4c3bfe32..4be3e46eb7f 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -1236,9 +1236,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libz-sys" @@ -1330,14 +1330,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys 0.48.0", ] [[package]] From 64e03c484b47cda5f0bc05fef2423f36f21000d5 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 11 Mar 2024 16:32:24 -0700 Subject: [PATCH 129/141] bump distro (#207375) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a889d68b52e..cc63ba11028 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "cace2b0e305fab89810eb1a0a2c98c50838aec5e", + "distro": "42961f5ae266361a063bc9849757d3b704f9ee82", "author": { "name": "Microsoft Corporation" }, From 6653159a71c6a2da55a48dc9ca77145ad66c1e63 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Mon, 11 Mar 2024 16:34:18 -0700 Subject: [PATCH 130/141] Relocate nb stickyscroll toggle action, fix toggle verbage, more generic run section args (#207373) relocate toggle action, fix toggle verbage, more generic section args --- .../parts/editor/breadcrumbsControl.ts | 2 +- .../browser/controller/layoutActions.ts | 35 +++++++- .../viewParts/notebookEditorStickyScroll.ts | 79 +++++-------------- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 1802e2eac64..e2fefb174e6 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -611,7 +611,7 @@ registerAction2(class ToggleBreadcrumb extends Action2 { category: Categories.View, toggled: { condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true), - title: localize('cmd.toggle2', "Breadcrumbs"), + title: localize('cmd.toggle2', "Toggle Breadcrumbs"), mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breadcrumbs") }, menu: [ diff --git a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index 2dfc3c83bc3..19f30d4185f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -6,9 +6,10 @@ import { Codicon } from 'vs/base/common/codicons'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -242,3 +243,35 @@ registerAction2(class NotebookWebviewResetAction extends Action2 { } } }); + +registerAction2(class ToggleNotebookStickyScroll extends Action2 { + constructor() { + super({ + id: 'notebook.action.toggleNotebookStickyScroll', + title: { + ...localize2('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + }, + category: Categories.View, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.stickyScroll.enabled', true), + title: localize('notebookStickyScroll', "Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + }, + menu: [ + { id: MenuId.CommandPalette }, + { + id: MenuId.NotebookStickyScrollContext, + group: 'notebookView', + order: 2 + } + ] + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('notebook.stickyScroll.enabled'); + return configurationService.updateValue('notebook.stickyScroll.enabled', newValue); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 1fb8e60d3db..ec47ab72931 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -10,10 +10,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; @@ -27,43 +24,9 @@ import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewM import { FoldingController } from 'vs/workbench/contrib/notebook/browser/controller/foldingController'; import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -export class ToggleNotebookStickyScroll extends Action2 { - - constructor() { - super({ - id: 'notebook.action.toggleNotebookStickyScroll', - title: { - ...localize2('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), - }, - category: Categories.View, - toggled: { - condition: ContextKeyExpr.equals('config.notebook.stickyScroll.enabled', true), - title: localize('notebookStickyScroll', "Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), - }, - menu: [ - { id: MenuId.CommandPalette }, - { - id: MenuId.NotebookStickyScrollContext, - group: 'notebookView', - order: 2 - } - ] - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const newValue = !configurationService.getValue('notebook.stickyScroll.enabled'); - return configurationService.updateValue('notebook.stickyScroll.enabled', newValue); - } -} - -type RunInSectionContext = { - target: HTMLElement; - currentStickyLines: Map; +type NotebookSectionArgs = { notebookEditor: INotebookEditor; + outlineEntry: OutlineEntry; }; export class RunInSectionStickyScroll extends Action2 { @@ -84,24 +47,20 @@ export class RunInSectionStickyScroll extends Action2 { }); } - override async run(accessor: ServicesAccessor, context: RunInSectionContext, ...args: any[]): Promise { - const selectedElement = context.target.parentElement; - const stickyLines: Map = context.currentStickyLines; - - const selectedOutlineEntry = Array.from(stickyLines.values()).find(entry => entry.line.element.contains(selectedElement))?.line.entry; - if (!selectedOutlineEntry) { + override async run(accessor: ServicesAccessor, context: NotebookSectionArgs, ...args: any[]): Promise { + const cell = context.outlineEntry.cell; + const idx = context.notebookEditor.getViewModel()?.getCellIndex(cell); + if (idx === undefined) { return; } + const length = context.notebookEditor.getViewModel()?.getFoldedLength(idx); + if (length === undefined) { + return; + } + const cells = context.notebookEditor.getCellsInRange({ start: idx, end: idx + length + 1 }); - const flatList: OutlineEntry[] = []; - selectedOutlineEntry.asFlatList(flatList); - - const cellViewModels = flatList.map(entry => entry.cell); const notebookEditor: INotebookEditor = context.notebookEditor; - notebookEditor.executeNotebookCells(cellViewModels); + notebookEditor.executeNotebookCells(cells); } } @@ -246,16 +205,21 @@ export class NotebookStickyScroll extends Disposable { private onContextMenu(e: MouseEvent) { const event = new StandardMouseEvent(DOM.getWindow(this.domNode), e); - const context: RunInSectionContext = { - target: event.target, - currentStickyLines: this.currentStickyLines, + const selectedElement = event.target.parentElement; + const selectedOutlineEntry = Array.from(this.currentStickyLines.values()).find(entry => entry.line.element.contains(selectedElement))?.line.entry; + if (!selectedOutlineEntry) { + return; + } + + const args: NotebookSectionArgs = { + outlineEntry: selectedOutlineEntry, notebookEditor: this.notebookEditor, }; this._contextMenuService.showContextMenu({ menuId: MenuId.NotebookStickyScrollContext, getAnchor: () => event, - menuActionOptions: { shouldForwardArgs: true, arg: context }, + menuActionOptions: { shouldForwardArgs: true, arg: args }, }); } @@ -538,5 +502,4 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList return newMap; } -registerAction2(ToggleNotebookStickyScroll); registerAction2(RunInSectionStickyScroll); From 14e109cb03125dfc2b9ebb328eb366878a052a63 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Mon, 11 Mar 2024 17:04:46 -0700 Subject: [PATCH 131/141] Add experimental chat welcome view (#207369) * Add experimental chat welcome view * Remove unused import * Enable chatWelcomeView creation on context key change --- src/vs/base/common/product.ts | 8 ++++ .../browser/chatContributionServiceImpl.ts | 45 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 3a57612651d..39954158abf 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -189,6 +189,7 @@ export interface IProductConfiguration { readonly commonlyUsedSettings?: string[]; readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust; readonly gitHubEntitlement?: IGitHubEntitlement; + readonly chatWelcomeView?: IChatWelcomeView; } export interface ITunnelApplicationConfig { @@ -302,3 +303,10 @@ export interface IGitHubEntitlement { confirmationMessage: string; confirmationAction: string; } + +export interface IChatWelcomeView { + welcomeViewId: string; + welcomeViewTitle: string; + welcomeViewContent: string; + when: string; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index fca2fe47519..323d2415026 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -7,8 +7,9 @@ import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -127,16 +128,56 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; + private readonly disposables = new DisposableStore(); + private _welcomeViewDescriptor?: IViewDescriptor; private _viewContainer: ViewContainer; private _registrationDisposables = new Map(); constructor( - @IChatContributionService readonly _chatContributionService: IChatContributionService + @IChatContributionService readonly _chatContributionService: IChatContributionService, + @IProductService readonly productService: IProductService, + @IContextKeyService readonly contextService: IContextKeyService ) { this._viewContainer = this.registerViewContainer(); + this.registerListeners(); this.handleAndRegisterChatExtensions(); } + private registerListeners() { + this.contextService.onDidChangeContext(e => { + + if (!this.productService.chatWelcomeView) { + return; + } + + const keys = new Set([this.productService.chatWelcomeView.when]); + if (e.affectsSome(keys)) { + const contextKeyExpr = ContextKeyExpr.equals(this.productService.chatWelcomeView.when, true); + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + if (this.contextService.contextMatchesRules(contextKeyExpr)) { + const viewId = this._chatContributionService.getViewIdForProvider(this.productService.chatWelcomeView.welcomeViewId); + + this._welcomeViewDescriptor = { + id: viewId, + name: { original: this.productService.chatWelcomeView.welcomeViewTitle, value: this.productService.chatWelcomeView.welcomeViewTitle }, + containerIcon: this._viewContainer.icon, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: this.productService.chatWelcomeView.welcomeViewId }]), + canToggleVisibility: false, + canMoveView: true, + order: 100 + }; + viewsRegistry.registerViews([this._welcomeViewDescriptor], this._viewContainer); + + viewsRegistry.registerViewWelcomeContent(viewId, { + content: this.productService.chatWelcomeView.welcomeViewContent, + }); + } else if (this._welcomeViewDescriptor) { + viewsRegistry.deregisterViews([this._welcomeViewDescriptor], this._viewContainer); + } + } + }, null, this.disposables); + } + private handleAndRegisterChatExtensions(): void { chatExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { From ac31a0ccddcc1b3480da8db3bef1f0f7ab545359 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:34:47 -0500 Subject: [PATCH 132/141] bump distro for copilot use of aiTextSearchProvider (#207378) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc63ba11028..f96e8a6967b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "42961f5ae266361a063bc9849757d3b704f9ee82", + "distro": "03722d24faeb8330c72b8c7aa0c3a7acf7d916b2", "author": { "name": "Microsoft Corporation" }, From 9c3cc8b9948d52f04836997a7de01dbd9bb2a7bf Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 12 Mar 2024 12:03:43 +1100 Subject: [PATCH 133/141] Avoid unnecessary workspace edits (#207377) --- extensions/ipynb/src/notebookAttachmentCleaner.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/ipynb/src/notebookAttachmentCleaner.ts b/extensions/ipynb/src/notebookAttachmentCleaner.ts index cad19f07b29..abe4e68fa85 100644 --- a/extensions/ipynb/src/notebookAttachmentCleaner.ts +++ b/extensions/ipynb/src/notebookAttachmentCleaner.ts @@ -103,7 +103,9 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { notebookEdits.push(metadataEdit); } } - + if (!notebookEdits.length) { + return; + } const workspaceEdit = new vscode.WorkspaceEdit(); workspaceEdit.set(e.notebook.uri, notebookEdits); @@ -229,7 +231,7 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { this.updateDiagnostics(cell.document.uri, diagnostics); - if (cell.index > -1 && !objectEquals(markdownAttachmentsInUse, cell.metadata.attachments)) { + if (cell.index > -1 && !objectEquals(markdownAttachmentsInUse || {}, cell.metadata.attachments || {})) { const updateMetadata: { [key: string]: any } = deepClone(cell.metadata); if (Object.keys(markdownAttachmentsInUse).length === 0) { updateMetadata.attachments = undefined; From 30f1a7103ba860dfce68cb7a8cc45a83904a6d0c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 12 Mar 2024 00:08:50 -0700 Subject: [PATCH 134/141] Pass references on code block document as metadata (#207382) Pass along code block references in the fragment of code block documents --- .../contrib/chat/browser/chatListRenderer.ts | 7 +++- .../contrib/chat/common/chatViewModel.ts | 2 +- .../chat/common/codeBlockModelCollection.ts | 42 +++++++++++++++---- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index f60b2549be8..ac4d6a1f2a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -870,8 +870,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { languageId ??= ''; const newText = this.fixCodeText(value, languageId); - const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model.id, codeBlockIndex++); + const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model, codeBlockIndex++); textModel.then(ref => { const model = ref.object.textEditorModel; if (languageId) { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 763773d71c2..092ff4adeca 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -8,6 +8,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; export class CodeBlockModelCollection extends Disposable { @@ -25,18 +26,18 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(sessionId: string, responseId: string, codeBlockIndex: number): Promise> | undefined { - const uri = this.getUri(sessionId, responseId, codeBlockIndex); + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): Promise> | undefined { + const uri = this.getUri(sessionId, chat, codeBlockIndex); return this._models.get(uri); } - getOrCreate(sessionId: string, responseId: string, codeBlockIndex: number): Promise> { - const existing = this.get(sessionId, responseId, codeBlockIndex); + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): Promise> { + const existing = this.get(sessionId, chat, codeBlockIndex); if (existing) { return existing; } - const uri = this.getUri(sessionId, responseId, codeBlockIndex); + const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, ref); return ref; @@ -47,7 +48,34 @@ export class CodeBlockModelCollection extends Disposable { this._models.clear(); } - private getUri(sessionId: string, responseId: string, index: number): URI { - return URI.from({ scheme: Schemas.vscodeChatCodeBlock, authority: sessionId, path: `/${responseId}/${index}` }); + private getUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI { + const metadata = this.getUriMetaData(chat); + return URI.from({ + scheme: Schemas.vscodeChatCodeBlock, + authority: sessionId, + path: `/${chat.id}/${index}`, + fragment: metadata ? JSON.stringify(metadata) : undefined, + }); + } + + private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) { + if (!isResponseVM(chat)) { + return undefined; + } + + return { + references: chat.contentReferences.map(ref => { + if (URI.isUri(ref.reference)) { + return { + uri: ref.reference.toJSON() + }; + } + + return { + uri: ref.reference.uri.toJSON(), + range: ref.reference.range, + }; + }) + }; } } From 7696df90266e0226fa9c3c97201dbf3452aeee14 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 12 Mar 2024 18:18:20 +1100 Subject: [PATCH 135/141] Avoid blocking event loop when saving nb (#207381) --- .../ipynb/src/notebookAttachmentCleaner.ts | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/extensions/ipynb/src/notebookAttachmentCleaner.ts b/extensions/ipynb/src/notebookAttachmentCleaner.ts index abe4e68fa85..32aae0c5d1e 100644 --- a/extensions/ipynb/src/notebookAttachmentCleaner.ts +++ b/extensions/ipynb/src/notebookAttachmentCleaner.ts @@ -81,36 +81,31 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { this._disposables.push(vscode.workspace.onWillSaveNotebookDocument(e => { if (e.reason === vscode.TextDocumentSaveReason.Manual) { this._delayer.dispose(); - - e.waitUntil(new Promise((resolve) => { - if (e.notebook.getCells().length === 0) { - return; + if (e.notebook.getCells().length === 0) { + return; + } + const notebookEdits: vscode.NotebookEdit[] = []; + for (const cell of e.notebook.getCells()) { + if (cell.kind !== vscode.NotebookCellKind.Markup) { + continue; } - const notebookEdits: vscode.NotebookEdit[] = []; - for (const cell of e.notebook.getCells()) { - if (cell.kind !== vscode.NotebookCellKind.Markup) { - continue; - } + const metadataEdit = this.cleanNotebookAttachments({ + notebook: e.notebook, + cell: cell, + document: cell.document + }); - const metadataEdit = this.cleanNotebookAttachments({ - notebook: e.notebook, - cell: cell, - document: cell.document - }); - - if (metadataEdit) { - notebookEdits.push(metadataEdit); - } + if (metadataEdit) { + notebookEdits.push(metadataEdit); } - if (!notebookEdits.length) { - return; - } - const workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.set(e.notebook.uri, notebookEdits); - - resolve(workspaceEdit); - })); + } + if (!notebookEdits.length) { + return; + } + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(e.notebook.uri, notebookEdits); + e.waitUntil(Promise.resolve(workspaceEdit)); } })); From 6c1e897687dfa223e4ba81669907e037eed415f9 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 12 Mar 2024 16:42:05 +0900 Subject: [PATCH 136/141] chore: update electron@28.2.6 (#207388) * chore: update electron@28.2.6 * chore: update distro --- .yarnrc | 4 +- build/checksums/electron.txt | 150 +++++++++++++++++------------------ cgmanifest.json | 4 +- package.json | 4 +- yarn.lock | 8 +- 5 files changed, 85 insertions(+), 85 deletions(-) diff --git a/.yarnrc b/.yarnrc index dc10cd8fae6..05efa60c84c 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.5" -ms_build_id "27336930" +target "28.2.6" +ms_build_id "27476517" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 72d5349d425..b77f73fb763 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -23d9bca1abd1c64d0bd47b9528b8db1b1f28c31e81188ecbed4e9cd18eab3545 *chromedriver-v28.2.5-darwin-arm64.zip -9215cf2196988c5f0e0a01fe1bdd827ab25f3a0895b6e9ff96185fed45be24d9 *chromedriver-v28.2.5-darwin-x64.zip -a27c39a8a9f02a630f4ea1218954e768791e44319ce34e99bb524d45aa956376 *chromedriver-v28.2.5-linux-arm64.zip -658bef49300d3183a34609391f64f3df6c9b07eb55886fa1378249e1170ac68e *chromedriver-v28.2.5-linux-armv7l.zip -14a285843587f251455a3ac69be5bebca7e7c3e934151a69dc8c10c943aaac49 *chromedriver-v28.2.5-linux-x64.zip -173112b71f363f1c434eb4bfe8356a5a4592a0580d8c434c2141f3a04de7695b *chromedriver-v28.2.5-mas-arm64.zip -b72902d8f4d886fef3f945e4a9dd707e18d52201a57e421a555cc166689955a7 *chromedriver-v28.2.5-mas-x64.zip -723cc0db4299d23c6be611b723187c857102749de2f2294bae09047b0d99cfd2 *chromedriver-v28.2.5-win32-arm64.zip -1c549de92e2d784cc2a2618d129e368d74e8da6497df7f5bcabfd2f834981f5d *chromedriver-v28.2.5-win32-ia32.zip -2df3c811c3ed8f22f28e740ffe0abf7c6d0c29d1874efa5290b75575a23d292b *chromedriver-v28.2.5-win32-x64.zip -a6e536d48e399f0961cb5de1e9cb0d3e534c4686fdf6fc79080e66516fdd5b6e *electron-api.json -746c5867227538235cff139e174a7b85fa49230a69350414bed7d1e6ae664cba *electron-v28.2.5-darwin-arm64-dsym-snapshot.zip -b5d00927dead894355c26cc581443735c252a71a53a363f3909f02b39ba1a38f *electron-v28.2.5-darwin-arm64-dsym.zip -6bb1356b72b5d3f8c3d25ef3f42a9ab8574498ab79299af056d8ac93972de72d *electron-v28.2.5-darwin-arm64-symbols.zip -87b17c403d355ba2eee43ee3a955c02069571617ef081b951272c1337ed5a2bb *electron-v28.2.5-darwin-arm64.zip -ff8b7d3073bbc1f26d83a224a79def7cfa652318b98d513603ac3d6e3ea56905 *electron-v28.2.5-darwin-x64-dsym-snapshot.zip -26057699098b6a4173c3f2550ec9a08d3edf4ce3aab5b351ca41c056f8f5ea5f *electron-v28.2.5-darwin-x64-dsym.zip -a467b38526c2c6c677dfe71898eaa8f3c8fccd7675bddfbfce6b095ebd66ea62 *electron-v28.2.5-darwin-x64-symbols.zip -a1ed37b654c48afe0fa8411d09b8644e121fa448d9cafa2ee6a3d2f5d8d36a4d *electron-v28.2.5-darwin-x64.zip -e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-arm64-debug.zip -cebd2961e8af1600ca307f530c8b89dafa934cfed356830f4b5c04c048ffc204 *electron-v28.2.5-linux-arm64-symbols.zip -2a24355c27b5d43a424caedfcfe3fc42aeea80e13977fd708537519d6850c372 *electron-v28.2.5-linux-arm64.zip -e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-armv7l-debug.zip -de46cbbe2c3eb4cd7d761ec57e85cd2077592b88586e1d9f45606df69a1e5dab *electron-v28.2.5-linux-armv7l-symbols.zip -2707f9fb7b7c6ea8038f8c67054cdfee4fb0bbaec36c20f3ffcca05cd5bbcd6c *electron-v28.2.5-linux-armv7l.zip -2567949ac356f53a5f145e6efaef1e1bc07dabfafa549765ebca54b33b0c298b *electron-v28.2.5-linux-x64-debug.zip -b067d8dfd0345129172628575684c7bc3d739843662aecc33ce4221a96fb7d48 *electron-v28.2.5-linux-x64-symbols.zip -1c1e972fa7daa54e1e54b642b2828495020367df5dcf1e3a4d4fc170980a8d6e *electron-v28.2.5-linux-x64.zip -23068f0cad14769f715147d1778da27a1850cdeea20c1ff7c7b1eb729b4d87b0 *electron-v28.2.5-mas-arm64-dsym-snapshot.zip -04640b64a0132f4606d31b89f8fa063b12be8936f0a2c546e05a992f23d00260 *electron-v28.2.5-mas-arm64-dsym.zip -cf1787c50932ef5c5309a8d80d7647495ddac4c71b6e0feb8ea547299c114ed4 *electron-v28.2.5-mas-arm64-symbols.zip -7f2339a92defc1808714bcdd0561430898e6a9f0cb8788a91bf178c10228fb2a *electron-v28.2.5-mas-arm64.zip -26eca4afd370e422c911c68268cf668ca43c10617c4e9cd53eaa564e698efd50 *electron-v28.2.5-mas-x64-dsym-snapshot.zip -ab928fcd3851651d9ef62fe4b62b48471a6673c603b70ca7f4049e72ad3bc2c2 *electron-v28.2.5-mas-x64-dsym.zip -c64232513b56b5b82f76b637a04f68fcfb7231ea8076d6dc1375aac8dac4a02c *electron-v28.2.5-mas-x64-symbols.zip -8d3a0988e699a42482079676ffba2ac50fbd74f1097d40cee0029ff5bc189144 *electron-v28.2.5-mas-x64.zip -7fcc54dc77dedbf4cb9eeaba1678ebf265649479c478e344c6dff27b1ec63b0b *electron-v28.2.5-win32-arm64-pdb.zip -31a69a6ed4e71b4fb047d19522dc32f9ff0f4b617536dcb7e8b562c9c583adbf *electron-v28.2.5-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-arm64-toolchain-profile.zip -68a6fc3411daaf604da0009df506a655587cb6e5cc19d6a1c47ce0b62fdb4ac6 *electron-v28.2.5-win32-arm64.zip -e74519411a678a9885bfb07acb5df85632f3de67d2fc54ccbd5ebd548edf84c8 *electron-v28.2.5-win32-ia32-pdb.zip -798261cfed077ec4afc965ab1a8e3fe4c976533ba86fa8f17cd69107bd1be3be *electron-v28.2.5-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-ia32-toolchain-profile.zip -41f23f86bf7aa19f67025af7db221b727a38ffc0b1c5661b305be7250ecb7abc *electron-v28.2.5-win32-ia32.zip -ee882c550a2889dd18f58bf0f5c5ee9a1dd0eb6ed29c4f9a359b0db5314d7965 *electron-v28.2.5-win32-x64-pdb.zip -197b7ab2ba961ded3260ae91cf57502c43c2cdaca3fc18b90ec6f4d4e08ba9ac *electron-v28.2.5-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-x64-toolchain-profile.zip -9235e039183fd62a6d37f70ae39a0f3a3ddb4e00e1474e6258343d1ad955c995 *electron-v28.2.5-win32-x64.zip -981a6c4d1030af6949405c3818b7332a16a959bd30970f5660e4975ccdf31789 *electron.d.ts -90b6c39e1ba7bbf0bccc3e009bcdbd4d8a57821f669229ab7847fd3d56cc8a66 *ffmpeg-v28.2.5-darwin-arm64.zip -3a736fed82b232624aeba8a33c02e1ce589214db0faf5984e216f8a72cbf713a *ffmpeg-v28.2.5-darwin-x64.zip -1674cdc15b72fb421ae4fd5afb217ef8968fb879db391343519764e2e77edb41 *ffmpeg-v28.2.5-linux-arm64.zip -6db75c7fe794f2a2edf3ff9b0d8ad6157d132c89e36e42580594a26f56658ae5 *ffmpeg-v28.2.5-linux-armv7l.zip -7fed2646cf8cce5c6c1afe4214b2d1ea12c89ed379c4b2cdb06cdadb14ceb4f6 *ffmpeg-v28.2.5-linux-x64.zip -fb3649690c496f4c6f884ddf94a7ff518278f6140c2487dd652256f53ed2e3fa *ffmpeg-v28.2.5-mas-arm64.zip -c1fee5ef3a550a5e9a652e251e6ec3677d156610670b54489b5da0c6b4007179 *ffmpeg-v28.2.5-mas-x64.zip -6745a8816159bc980f1ccb4d28c2f02f70cb5e2faf6423e0924d890ca6353dd8 *ffmpeg-v28.2.5-win32-arm64.zip -e1046c0280a7833227963b43d468973d646bd38ae100a298bb28d6a229e723b2 *ffmpeg-v28.2.5-win32-ia32.zip -a189c9c2317f011735e6d1cb743a78536f45a41f18a16c81d5294ca933d9519a *ffmpeg-v28.2.5-win32-x64.zip -29a594a16cdf3585299e7c585bae2ea007e108a72a96b9e2a95d149e7dc381d6 *hunspell_dictionaries.zip -7c1263edb062c07c2fb589812788f70772629c1798abd1481205cf8cdb999120 *libcxx-objects-v28.2.5-linux-arm64.zip -fc75baa4308a58048fe846b9fe78bba362a34becc5bf32a776f15933f4beaedf *libcxx-objects-v28.2.5-linux-armv7l.zip -1bedd5f8f3c897b0ca3cb263620cc4a8fff7001fcd6318a12c2c4cd9e922b35f *libcxx-objects-v28.2.5-linux-x64.zip -f6f37ee5bf297959c4fdec9bb77637310e6c8a85c7defadcbd660507a9e63728 *libcxx_headers.zip -8849467a7e670355b9cab854d66a09d57e9d91e8881034b07eb71f9d9928eb18 *libcxxabi_headers.zip -ff875bb59ecc8bf01b618d61d4a8378e133ea2c2571653828c9ea08773f5d776 *mksnapshot-v28.2.5-darwin-arm64.zip -30c1f135220d783b08a70bc7992877431e320543837ce0d90102039a945023aa *mksnapshot-v28.2.5-darwin-x64.zip -289e6b5feabe9ea22c10fc0fd0afcde59670506df1af6f1e87dc4dab5cbada29 *mksnapshot-v28.2.5-linux-arm64-x64.zip -e857fe518df1063308514224f82d13ffc24bb661b22d9a8a10a915a69830037b *mksnapshot-v28.2.5-linux-armv7l-x64.zip -a14af21de32fbfdf5d40402b52e7ff4858682cf3958fd6898ea30df331164004 *mksnapshot-v28.2.5-linux-x64.zip -8531b3ed3b47ed0a34a317be2fd03a573ee38e719aeddeeb0a6b3d5c36268ffa *mksnapshot-v28.2.5-mas-arm64.zip -71c978fffa8cb4a3f13842a8eadcb29e0782a648492204ebba08edb23b1fa297 *mksnapshot-v28.2.5-mas-x64.zip -b1ce8db39866860a6853c9a8874224c757a2b3086a451b7b1c30511615457166 *mksnapshot-v28.2.5-win32-arm64-x64.zip -c9a7f82fcd320c52f111d18b7fe6aa9ec739f94a13a7e8e04d22e6706a889c4b *mksnapshot-v28.2.5-win32-ia32.zip -493d0eabacf33c9d51305cf40bf7590901eb4e38d53308a76ae05db5af0a8468 *mksnapshot-v28.2.5-win32-x64.zip +2cd042f38fd13cbb3ed0e7205c6c892cd5f04fd4992d18da363b8f0df9dda3eb *chromedriver-v28.2.6-darwin-arm64.zip +05bc772ecb5728cde1efed2308074ad53a4abfe7c541a82d6fef62d3350c6cf4 *chromedriver-v28.2.6-darwin-x64.zip +4c7ea31be89009fcedfe8e3619be61bec6056c8bb9ea93b4e6a5deec791f8c55 *chromedriver-v28.2.6-linux-arm64.zip +ae61e86c512dff5108f2240018c3b549b57e25f3f31e822effb7f1d5a53cd474 *chromedriver-v28.2.6-linux-armv7l.zip +d2eac837adf3691abfab267d5e5f2428450c3ca506d74e47382bd0ae73755a4a *chromedriver-v28.2.6-linux-x64.zip +326f6f4ce44e42bae98894eb3f3ef125fe887a1188ce98d8cc1e8b68862283fe *chromedriver-v28.2.6-mas-arm64.zip +4cb08690d4db116f115e5da2f2d9ed9ccb287a33a8c9cb7264dba1329117f979 *chromedriver-v28.2.6-mas-x64.zip +ce1124ac3e5b91efc78d95260e5ecb001b362f12f1c9d2abc71fc3e8140aefb1 *chromedriver-v28.2.6-win32-arm64.zip +1a36b630b828953873a102c118d7954409de7ae0e40bdcb325baca0915fde4ff *chromedriver-v28.2.6-win32-ia32.zip +7e138e53e1acada2047c9adda42ee3760397cda56f7c73f30b48f69c51fb136f *chromedriver-v28.2.6-win32-x64.zip +f8809dc99407cc14bdc6579a6205d391ecf285a6d9ef49a34d529371616cd032 *electron-api.json +3bd369be1ce7175a637eb5531317c49c13287152cae4e0cfb875acdceee92fe3 *electron-v28.2.6-darwin-arm64-dsym-snapshot.zip +0020309287b4eef7cc59b761a1d604af80cca6d195cfecea5b97b834ba808d2a *electron-v28.2.6-darwin-arm64-dsym.zip +b1aeb1b30a965cf439456beaa3e99228437f3f9f91ddbbfa27a1695143a8a892 *electron-v28.2.6-darwin-arm64-symbols.zip +432ef2d5767991347c9452961e392182baa761d0b8b23483c1117a8c75bf18e9 *electron-v28.2.6-darwin-arm64.zip +c4723e680bf78ebe7e4a151d0b68c8e698985c36007237e9c5ffbd3976451519 *electron-v28.2.6-darwin-x64-dsym-snapshot.zip +86845958cedf3af045f07fd287066678e0ff73a8caf29c8032e8def0d3277b23 *electron-v28.2.6-darwin-x64-dsym.zip +450f7324fb9b0baed557133af50c8772a4b3e33f1288a7e732f7cc8fbd9df30d *electron-v28.2.6-darwin-x64-symbols.zip +524d710d21d64b539e568946debb3659b8e8071ead56c4b1a598c7c76fc32089 *electron-v28.2.6-darwin-x64.zip +c6ecf165f51d7da20278324a7454cc5119e6e546527dc9f21e7d4701f062443d *electron-v28.2.6-linux-arm64-debug.zip +cb495ec65c3a5cb6639a2ff1110f588cc82df241982e5cbb91932990de723772 *electron-v28.2.6-linux-arm64-symbols.zip +cdc832c6e337a2241bec78b7130f21c6db01d90d0ef93cd3c934f220319fa696 *electron-v28.2.6-linux-arm64.zip +c6ecf165f51d7da20278324a7454cc5119e6e546527dc9f21e7d4701f062443d *electron-v28.2.6-linux-armv7l-debug.zip +9eb155513a0a6f6fa518c2c768e0cc483a3e35c7beb7b657211df7bcf33ad144 *electron-v28.2.6-linux-armv7l-symbols.zip +6e340b9468950d8d3b5a9bf7840622403346f043af3f25471beff32212d227ce *electron-v28.2.6-linux-armv7l.zip +51567b886d0510726e733d9ecb33f32f14c78ee8cdedfa56adae26c0ac59b890 *electron-v28.2.6-linux-x64-debug.zip +fd6e2bc61e6df6113a74503d60bdea23d6734ba75bc270fa87f2a99a472d2e22 *electron-v28.2.6-linux-x64-symbols.zip +a5ec62621ccd0cd4636dc290a0406abc706c2900b518b085bff2312a5ee1dc6f *electron-v28.2.6-linux-x64.zip +1fb074339d42ef399254199418849f0fd591ba6bb203ab0570be192d7225921a *electron-v28.2.6-mas-arm64-dsym-snapshot.zip +3a8228698d1a85103eb3958de0ba8d77f1129a4eca44227a46dc70eda3ce2abc *electron-v28.2.6-mas-arm64-dsym.zip +3668d2aa7d00679f93106b1feb1dab4f1284bf5c6a041aa47284693786b3ad08 *electron-v28.2.6-mas-arm64-symbols.zip +ba40b18e6964fa96a72a86984032b534b94596b5a29418d286fba090f6ec8076 *electron-v28.2.6-mas-arm64.zip +dc7d071fb39d89c65745f6a567011959c4cd32e60e95cb92d970bc0ca89da26f *electron-v28.2.6-mas-x64-dsym-snapshot.zip +812909c73e1ebcb121e7874ae2250ed55ce58e3ef651fcfac9bd92284b1f6d69 *electron-v28.2.6-mas-x64-dsym.zip +fd720ee5353c20ff1ff0dc1b8eeaa64f28f7860268d5f8528d468ced0375086b *electron-v28.2.6-mas-x64-symbols.zip +9ce774a52e32a7df11c6ca20ae766303a33f1fd9000c628238fe93426b73216e *electron-v28.2.6-mas-x64.zip +952360b9cc257c145de62111cf9f0569892b3dfde3d4f8246b4025d8931c0377 *electron-v28.2.6-win32-arm64-pdb.zip +095500f4db01a8448cf7263a9db053446d88c08e1f6abd9a84323b7c45bd5a25 *electron-v28.2.6-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-arm64-toolchain-profile.zip +99b24366555381bdaf35e4b85956941c859afbbfc52b5cc66bbda7ace4bcdc26 *electron-v28.2.6-win32-arm64.zip +85f92b7d9f5689c92216c71b3e76a3e1181f3b74b1a30649c5870126d197c057 *electron-v28.2.6-win32-ia32-pdb.zip +276b143933a186e397820424fddc6d0488d3293828e76273d64a6d642b64b67d *electron-v28.2.6-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-ia32-toolchain-profile.zip +8f4547b567c5e88b7b5c08381d9da21210cbf796cf4b8348f2f15c139d03dc3a *electron-v28.2.6-win32-ia32.zip +1e875caf77e8ba4f622743e015522b1bef6b73eacebbfb00b9f62cd1fd46a3d3 *electron-v28.2.6-win32-x64-pdb.zip +c57843add2a3106247c3e16b5a246bfb43a046a114826f222004d72c54ab6e0b *electron-v28.2.6-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-x64-toolchain-profile.zip +cc27e8af85c8cde97cc53204b612365f3b1c53215e19bb5b6f303ea9491b4953 *electron-v28.2.6-win32-x64.zip +be5b134abac3cb2f771246712a564080b2e63475fe9f09accc7acb6ade03af3f *electron.d.ts +767539ad20af8cda91da9bf35183ddaea7a09aa3ee8274d2677f407502f24295 *ffmpeg-v28.2.6-darwin-arm64.zip +af8422c1596adf13887cac74aa185d3c84787174af305a49e558664162c0bbed *ffmpeg-v28.2.6-darwin-x64.zip +8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.6-linux-arm64.zip +51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.6-linux-armv7l.zip +acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.6-linux-x64.zip +d478d239203f337f146ba2d6f5af6640a82a8591faac23017f8709e0fbb61d8a *ffmpeg-v28.2.6-mas-arm64.zip +77bb31ee80979dce6b1ee786ab39eba7dd56dbdf29101e7046ee8cea1938e350 *ffmpeg-v28.2.6-mas-x64.zip +fc406f7a4239d5c37d4dbc44907184213b7e07de9d39796cbef7eaa4ead92549 *ffmpeg-v28.2.6-win32-arm64.zip +abd92844333712e2a2a891b2679cbaf434daf7aa50c371bfccccd553d2394300 *ffmpeg-v28.2.6-win32-ia32.zip +c5ce83bdfeb037f315bea8c97bfae344ebbec255fd173a7a769fd276b2bdbf28 *ffmpeg-v28.2.6-win32-x64.zip +cc27058b50af2fe95070f52aa72e417f27f440cac2ae0471f31af061181272fd *hunspell_dictionaries.zip +b593c7f79c5fd49794dcf260ebd8e5b757313b467d3f671c5d2422f7ed3829f2 *libcxx-objects-v28.2.6-linux-arm64.zip +92a9f593ccb41c5507c0be01f1ec061d4090290e45c9b6aa003070b4b8fcb839 *libcxx-objects-v28.2.6-linux-armv7l.zip +fce1088e2bbc3bbcacae1741c2f7f2508ddf0e00f41450ff96d83df655ee431e *libcxx-objects-v28.2.6-linux-x64.zip +ee7ad0db6eb01ee72a70bc6ecf27428d1fd31ab52329fb75aa2b2a9702b1c1d7 *libcxx_headers.zip +1a2473c8e94c23a2c00a580c1ae379e2e74cae89ccf9dae977ceb9ba44658801 *libcxxabi_headers.zip +9365c442b6bbce858814028fed11a79a518e64d433d01821264afdf5492f9308 *mksnapshot-v28.2.6-darwin-arm64.zip +8410f72e3696691cb38ea31acfc6d501291df43bd20636059aaafbef9dc7757d *mksnapshot-v28.2.6-darwin-x64.zip +e081547def25c9af1f1ace4aa3ed0ee175bae7c401c92ee42988981d605cb8a7 *mksnapshot-v28.2.6-linux-arm64-x64.zip +c6b2d51a82fd10a04532f33385c78921dd85722a9fe3de107ab4809df08ae0e8 *mksnapshot-v28.2.6-linux-armv7l-x64.zip +dc80bc57f86e361e0769e1ed62b1d083c0dd1d160c6fcb2c10ae821a83ae6333 *mksnapshot-v28.2.6-linux-x64.zip +9ff56ed57c48df1e449c334bbe61874bd495d92978924565fdd2ec99e5be7a54 *mksnapshot-v28.2.6-mas-arm64.zip +fb04465058adad0e56f7c3dfa6d9d85b249554595925669e7454348e28490e02 *mksnapshot-v28.2.6-mas-x64.zip +681633334d9eaab61ab95c3718ba98e7bf339615b6189d51b0782b77f5e8eaea *mksnapshot-v28.2.6-win32-arm64-x64.zip +d3986690b413d43cc6a003f8e09fd07bf213eb5f006f3419e9615fcc09d4891d *mksnapshot-v28.2.6-win32-ia32.zip +b26942469d96148f7ec410996d9e4c34c3c013a441e568b05c87e36a3d9ae441 *mksnapshot-v28.2.6-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index e0a02fcc97d..19734908f1a 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "6544cec6864be60f577c1fcd41fa646c4d0192aa" + "commitHash": "2977fc4025fbc4c02ae9e87e480a94062b2ca4da" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.5" + "version": "28.2.6" }, { "component": { diff --git a/package.json b/package.json index f96e8a6967b..755da532d0b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "03722d24faeb8330c72b8c7aa0c3a7acf7d916b2", + "distro": "38430237b07b770e03ca457b74f3d48d65a0c0d7", "author": { "name": "Microsoft Corporation" }, @@ -149,7 +149,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.5", + "electron": "28.2.6", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/yarn.lock b/yarn.lock index 8dfa062b75b..9463669ba0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3563,10 +3563,10 @@ electron-to-chromium@^1.4.648: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== -electron@28.2.5: - version "28.2.5" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.5.tgz#d8e85306e8c51456042223a51f560f6ada565dc8" - integrity sha512-qlvQkDNVAzN647NpiJJw7GYJqE0NwK4+1evkhrQ0Xv6Qgab1EtN50G4oDr4/x/+O5pGUG2P5d3isXu+37O3RDw== +electron@28.2.6: + version "28.2.6" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.6.tgz#ec4958ff759009e3eb2c9489df5eb02f989f06bf" + integrity sha512-RuhbW+ifvh3DqnVlHCcCKhKIFOxTktq1GN1gkIkEZ8y5LEZfcjOkxB2s6Fd1S6MzsMZbiJti+ZJG5hXS4SDVLQ== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" From 6c8ae49b0fb5a8b50457e3758a4abce8389238e2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 12 Mar 2024 08:46:17 +0100 Subject: [PATCH 137/141] watcher - introduce and use `onDidWatchFail` event (#207320) --- .../files/node/watcher/baseWatcher.ts | 134 ++++++++---------- .../node/watcher/nodejs/nodejsWatcher.ts | 2 +- .../node/watcher/nodejs/nodejsWatcherLib.ts | 26 ++-- .../node/watcher/parcel/parcelWatcher.ts | 33 +++-- src/vs/platform/files/node/watcher/watcher.ts | 17 +-- .../node/nodejsWatcher.integrationTest.ts | 122 +++++++++------- .../node/parcelWatcher.integrationTest.ts | 56 +++++--- 7 files changed, 210 insertions(+), 180 deletions(-) diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 845d7bbe7c8..368da259162 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { watchFile, unwatchFile, Stats } from 'fs'; -import { Disposable, DisposableMap, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ILogMessage, IUniversalWatchRequest, IWatcher } from 'vs/platform/files/common/watcher'; import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; @@ -18,47 +18,45 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { protected readonly _onDidLogMessage = this._register(new Emitter()); readonly onDidLogMessage = this._onDidLogMessage.event; - private mapWatchMissingRequestPathToCorrelationId = this._register(new DisposableMap()); + protected readonly _onDidWatchFail = this._register(new Emitter()); + private readonly onDidWatchFail = this._onDidWatchFail.event; private allWatchRequests = new Set(); - private suspendedWatchRequests = new Set(); + private readonly suspendedWatchRequests = this._register(new DisposableMap()); - protected readonly missingRequestPathPollingInterval: number | undefined; + protected readonly suspendedWatchRequestPollingInterval: number | undefined; + + constructor() { + super(); + + this._register(this.onDidWatchFail(request => this.handleDidWatchFail(request))); + } + + private handleDidWatchFail(request: IUniversalWatchRequest): void { + if (!this.supportsRequestSuspendResume(request)) { + return; + } + + this.suspendWatchRequest(request); + } + + protected supportsRequestSuspendResume(request: IUniversalWatchRequest): boolean { + + // For now, limit failed watch monitoring to requests with a correlationId + // to experiment with this feature in a controlled way. Monitoring requests + // requires us to install polling watchers (via `fs.watchFile()`) and thus + // should be used sparingly. + + return typeof request.correlationId === 'number'; + } async watch(requests: IUniversalWatchRequest[]): Promise { this.allWatchRequests = new Set([...requests]); - const correlationIds = new Set(); - for (const request of requests) { - - // Request with correlation: watch request path to support - // watching paths that do not exist yet or are potentially - // being deleted and recreated. - // - // We are not doing this for all watch requests yet to see - // how it goes, thus its limitd to correlated requests. - - if (typeof request.correlationId === 'number') { - correlationIds.add(request.correlationId); - - if (!this.mapWatchMissingRequestPathToCorrelationId.has(request.correlationId)) { - this.mapWatchMissingRequestPathToCorrelationId.set(request.correlationId, this.watchMissingRequestPath(request)); - } - } - } - - // Remove all watched correlated paths that are no longer - // needed because the request is no longer there - for (const [correlationId] of this.mapWatchMissingRequestPathToCorrelationId) { - if (!correlationIds.has(correlationId)) { - this.mapWatchMissingRequestPathToCorrelationId.deleteAndDispose(correlationId); - } - } - - // Remove all suspended requests that are no longer needed - for (const request of this.suspendedWatchRequests) { + // Remove all suspended watch requests that are no longer watched + for (const [request] of this.suspendedWatchRequests) { if (!this.allWatchRequests.has(request)) { - this.suspendedWatchRequests.delete(request); + this.suspendedWatchRequests.deleteAndDispose(request); } } @@ -69,19 +67,33 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { return this.doWatch(Array.from(this.allWatchRequests).filter(request => !this.suspendedWatchRequests.has(request))); } - private watchMissingRequestPath(request: IUniversalWatchRequest): IDisposable { - if (typeof request.correlationId !== 'number') { - return Disposable.None; // for now limit this to correlated watch requests only (reduces surface) + private suspendWatchRequest(request: IUniversalWatchRequest): void { + if (this.suspendedWatchRequests.has(request)) { + return; // already suspended } - const that = this; - const resource = URI.file(request.path); + const disposables = new DisposableStore(); + this.suspendedWatchRequests.set(request, disposables); + + this.monitorSuspendedWatchRequest(request, disposables); + + this.updateWatchers(); + } + + private resumeWatchRequest(request: IUniversalWatchRequest): void { + this.suspendedWatchRequests.deleteAndDispose(request); + + this.updateWatchers(); + } + + private monitorSuspendedWatchRequest(request: IUniversalWatchRequest, disposables: DisposableStore) { + const resource = URI.file(request.path); + const that = this; - let disposed = false; let pathNotFound = false; const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => { - if (disposed) { + if (disposables.isDisposed) { return; // return early if already disposed } @@ -91,8 +103,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { pathNotFound = currentPathNotFound; // Watch path created: resume watching request - if ( - (previousPathNotFound && !currentPathNotFound) || // file was created + if ((previousPathNotFound && !currentPathNotFound) || // file was created (oldPathNotFound && !currentPathNotFound && !previousPathNotFound) // file was created from a rename ) { this.trace(`fs.watchFile() detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); @@ -102,47 +113,27 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { that._onDidChangeFile.fire([event]); this.traceEvent(event, request); - this.suspendedWatchRequests.delete(request); - this.updateWatchers(); - } - - // Watch path deleted or never existed: suspend watching request - else if (currentPathNotFound) { - this.trace(`fs.watchFile() detected ${request.path} not found, suspending watcher (correlationId: ${request.correlationId})`); - - if (!previousPathNotFound) { - const event: IFileChange = { resource, type: FileChangeType.DELETED, cId: request.correlationId }; - that._onDidChangeFile.fire([event]); - this.traceEvent(event, request); - } - - this.suspendedWatchRequests.add(request); - this.updateWatchers(); + // Resume watching + this.resumeWatchRequest(request); } }; this.trace(`starting fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); try { - watchFile(request.path, { persistent: false, interval: this.missingRequestPathPollingInterval }, watchFileCallback); + watchFile(request.path, { persistent: false, interval: this.suspendedWatchRequestPollingInterval }, watchFileCallback); } catch (error) { this.warn(`fs.watchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); - - return Disposable.None; } - return toDisposable(() => { + disposables.add(toDisposable(() => { this.trace(`stopping fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); - disposed = true; - - this.suspendedWatchRequests.delete(request); - try { unwatchFile(request.path, watchFileCallback); } catch (error) { this.warn(`fs.unwatchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); } - }); + })); } private isPathNotFound(stats: Stats): boolean { @@ -150,12 +141,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } async stop(): Promise { - this.mapWatchMissingRequestPathToCorrelationId.clearAndDisposeAll(); - this.suspendedWatchRequests.clear(); - } - - protected shouldRestartWatching(request: IUniversalWatchRequest): boolean { - return typeof request.correlationId !== 'number'; + this.suspendedWatchRequests.clearAndDisposeAll(); } protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void { diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index 1cbbe49f025..c3a04a4b90b 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -76,7 +76,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { private startWatching(request: INonRecursiveWatchRequest): void { // Start via node.js lib - const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); + const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); // Remember as watcher instance const watcher: INodeJSWatcherInstance = { request, instance }; diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 8f0f7d6a87f..82d4f43f58c 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -57,9 +57,10 @@ export class NodeJSFileWatcherLibrary extends Disposable { readonly ready = this.watch(); constructor( - private request: INonRecursiveWatchRequest, - private onDidFilesChange: (changes: IFileChange[]) => void, - private onLogMessage?: (msg: ILogMessage) => void, + private readonly request: INonRecursiveWatchRequest, + private readonly onDidFilesChange: (changes: IFileChange[]) => void, + private readonly onDidWatchFail?: () => void, + private readonly onLogMessage?: (msg: ILogMessage) => void, private verboseLogging?: boolean ) { super(); @@ -76,13 +77,14 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Watch via node.js const stat = await Promises.stat(realPath); this._register(await this.doWatch(realPath, stat.isDirectory())); - } catch (error) { if (error.code !== 'ENOENT') { this.error(error); } else { this.trace(error); } + + this.onDidWatchFail?.(); } } @@ -164,9 +166,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { watcher.on('error', (code: number, signal: string) => { this.error(`Failed to watch ${path} for changes using fs.watch() (${code}, ${signal})`); - // The watcher is no longer functional reliably - // so we go ahead and dispose it - this.dispose(); + this.onDidWatchFail?.(); }); watcher.on('change', (type, raw) => { @@ -230,9 +230,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { if (changedFileName === pathBasename && !await Promises.exists(path)) { this.warn('Watcher shutdown because watched path got deleted'); - // The watcher is no longer functional reliably - // so we go ahead and dispose it - this.dispose(); + this.onDidWatchFail?.(); return; } @@ -335,7 +333,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // we will loose this event. this.fileChangesAggregator.flush(); - this.dispose(); + this.onDidWatchFail?.(); } }, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY); @@ -352,9 +350,13 @@ export class NodeJSFileWatcherLibrary extends Disposable { } }); } catch (error) { - if (await Promises.exists(path) && !cts.token.isCancellationRequested) { + if (!cts.token.isCancellationRequested && await Promises.exists(path)) { this.error(`Failed to watch ${path} for changes using fs.watch() (${error.toString()})`); } + + this.onDidWatchFail?.(); + + return Disposable.None; } return toDisposable(() => { diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 8a4b6404da2..96096f35997 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -11,7 +11,7 @@ import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } fro import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { randomPath } from 'vs/base/common/extpath'; +import { randomPath, isEqual } from 'vs/base/common/extpath'; import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; @@ -133,7 +133,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { return !normalizedRequests.find(normalizedRequest => { - return normalizedRequest.path === request.path && + return isEqual(normalizedRequest.path, request.path, !isLinux) && patternsEquals(normalizedRequest.excludes, request.excludes) && patternsEquals(normalizedRequest.includes, request.includes) && normalizedRequest.pollingInterval === request.pollingInterval; @@ -295,6 +295,8 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { this.onUnexpectedError(error, watcher); instance.complete(undefined); + + this._onDidWatchFail.fire(request); }); } @@ -439,7 +441,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { let rootDeleted = false; for (const event of events) { - if (event.type === FileChangeType.DELETED && event.resource.fsPath === watcher.request.path) { + if (event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux)) { // Explicitly exclude changes to root if we have any // to avoid VS Code closing all opened editors which @@ -459,10 +461,21 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { private onWatchedPathDeleted(watcher: IParcelWatcherInstance): void { this.warn('Watcher shutdown because watched path got deleted', watcher); - if (!this.shouldRestartWatching(watcher.request)) { - return; // return if this deletion is handled outside - } + this._onDidWatchFail.fire(watcher.request); + // Do monitoring of the request path parent unless this request + // can be handled via suspend/resume in the super class + // + // TODO@bpasero we should remove this logic in favor of the + // support in the super class so that we have 1 consistent + // solution for handling this. + + if (!this.supportsRequestSuspendResume(watcher.request)) { + this.legacyMonitorRequest(watcher); + } + } + + private legacyMonitorRequest(watcher: IParcelWatcherInstance): void { const parentPath = dirname(watcher.request.path); if (existsSync(parentPath)) { this.trace('Trying to watch on the parent path to restart the watcher...', watcher); @@ -474,7 +487,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // Watcher path came back! Restart watching... for (const { resource, type } of changes) { - if (resource.fsPath === watcher.request.path && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { + if (isEqual(resource.fsPath, watcher.request.path, !isLinux) && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { if (this.isPathValid(watcher.request.path)) { this.warn('Watcher restarts because watched path got created again', watcher); @@ -488,7 +501,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } } } - }, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); + }, undefined, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); // Make sure to stop watching when the watcher is disposed watcher.token.onCancellationRequested(() => nodeWatcher.dispose()); @@ -626,12 +639,16 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } catch (error) { this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`); + this._onDidWatchFail.fire(request); + continue; } } // Check for invalid paths if (validatePaths && !this.isPathValid(request.path)) { + this._onDidWatchFail.fire(request); + continue; } diff --git a/src/vs/platform/files/node/watcher/watcher.ts b/src/vs/platform/files/node/watcher/watcher.ts index e266239cb65..d0b563e540c 100644 --- a/src/vs/platform/files/node/watcher/watcher.ts +++ b/src/vs/platform/files/node/watcher/watcher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { INonRecursiveWatchRequest, IRecursiveWatchRequest, IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; +import { IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; import { Event } from 'vs/base/common/event'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; @@ -20,20 +20,9 @@ export class UniversalWatcher extends Disposable implements IUniversalWatcher { readonly onDidError = Event.any(this.recursiveWatcher.onDidError, this.nonRecursiveWatcher.onDidError); async watch(requests: IUniversalWatchRequest[]): Promise { - const recursiveWatchRequests: IRecursiveWatchRequest[] = []; - const nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = []; - - for (const request of requests) { - if (request.recursive) { - recursiveWatchRequests.push(request); - } else { - nonRecursiveWatchRequests.push(request); - } - } - await Promises.settled([ - this.recursiveWatcher.watch(recursiveWatchRequests), - this.nonRecursiveWatcher.watch(nonRecursiveWatchRequests) + this.recursiveWatcher.watch(requests.filter(request => request.recursive)), + this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive)) ]); } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 28e9ded4c54..259b8a4e196 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -9,11 +9,11 @@ import { Promises, RimRafMode } from 'vs/base/node/pfs'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileChangeType } from 'vs/platform/files/common/files'; import { INonRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; -import { NodeJSFileWatcherLibrary, watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; +import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { getDriveLetter } from 'vs/base/common/extpath'; import { ltrim } from 'vs/base/common/strings'; -import { DeferredPromise } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; import { FileAccess } from 'vs/base/common/network'; @@ -31,34 +31,20 @@ import { Emitter, Event } from 'vs/base/common/event'; class TestNodeJSWatcher extends NodeJSWatcher { - protected override readonly missingRequestPathPollingInterval = 100; + protected override readonly suspendedWatchRequestPollingInterval = 100; private readonly _onDidWatch = this._register(new Emitter()); readonly onDidWatch = this._onDidWatch.event; + readonly onWatchFail = this._onDidWatchFail.event; + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { await super.doWatch(requests); - await this.whenReady(); - - this._onDidWatch.fire(); - } - - async whenReady(): Promise { for (const [, watcher] of this.watchers) { await watcher.instance.ready; } - } - } - class TestNodeJSFileWatcherLibrary extends NodeJSFileWatcherLibrary { - - private readonly _whenDisposed = new DeferredPromise(); - readonly whenDisposed = this._whenDisposed.p; - - override dispose(): void { - super.dispose(); - - this._whenDisposed.complete(); + this._onDidWatch.fire(); } } @@ -517,27 +503,6 @@ import { Emitter, Event } from 'vs/base/common/event'; await watcher.watch([{ path: invalidPath, excludes: [], recursive: false }]); }); - (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('deleting watched path is handled properly (folder watch)', async function () { - const watchedPath = join(testDir, 'deep'); - - const watcher = new TestNodeJSFileWatcherLibrary({ path: watchedPath, excludes: [], recursive: false }, changes => { }); - await watcher.ready; - - // Delete watched path and ensure watcher is now disposed - Promises.rm(watchedPath, RimRafMode.UNLINK); - await watcher.whenDisposed; - }); - - test('deleting watched path is handled properly (file watch)', async function () { - const watchedPath = join(testDir, 'lorem.txt'); - const watcher = new TestNodeJSFileWatcherLibrary({ path: watchedPath, excludes: [], recursive: false }, changes => { }); - await watcher.ready; - - // Delete watched path and ensure watcher is now disposed - Promises.unlink(watchedPath); - await watcher.whenDisposed; - }); - test('watchFileContents', async function () { const watchedPath = join(testDir, 'lorem.txt'); @@ -571,9 +536,41 @@ import { Emitter, Event } from 'vs/base/common/event'; await basicCrudTest(join(testDir, 'otherNewFile.txt'), undefined, null, 3); }); + test('watching missing path emits watcher fail event', async function () { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'missing'); + watcher.watch([{ path: folderPath, excludes: [], recursive: true }]); + + await onDidWatchFail; + }); + + test('deleting watched path emits watcher fail event (file watch)', async function () { + const filePath = join(testDir, 'lorem.txt'); + + await watcher.watch([{ path: filePath, excludes: [], recursive: false }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + Promises.unlink(filePath); + await onDidWatchFail; + }); + + (isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail event (folder watch)', async function () { + const folderPath = join(testDir, 'deep'); + + await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + Promises.rm(folderPath, RimRafMode.UNLINK); + await onDidWatchFail; + }); + test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () { const filePath = join(testDir, 'not-found.txt'); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + await onDidWatchFail; await basicCrudTest(filePath, undefined, 1, undefined, true); await basicCrudTest(filePath, undefined, 1, undefined, true); @@ -583,49 +580,64 @@ import { Emitter, Event } from 'vs/base/common/event'; const filePath = join(testDir, 'lorem.txt'); await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await basicCrudTest(filePath, true, 1); + await onDidWatchFail; + await basicCrudTest(filePath, undefined, 1, undefined, true); }); test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async function () { + let onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const folderPath = join(testDir, 'not-found'); await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + await onDidWatchFail; let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + let onDidWatch = Event.toPromise(watcher.onDidWatch); await Promises.mkdir(folderPath); await changeFuture; - await Event.toPromise(watcher.onDidWatch); + await onDidWatch; const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath, undefined, 1); - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); - await Promises.rmdir(folderPath); - await changeFuture; + if (!isMacintosh) { // macOS does not report DELETE events for folders + onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rmdir(folderPath); + await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); - await Promises.mkdir(folderPath); - await changeFuture; - await Event.toPromise(watcher.onDidWatch); + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; - await basicCrudTest(filePath, undefined, 1); + await timeout(500); // somehow needed on Linux + + await basicCrudTest(filePath, undefined, 1); + } }); - test('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () { + (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () { const folderPath = join(testDir, 'deep'); await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath, undefined, 1); - let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await Promises.rm(folderPath); - await changeFuture; + await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + const onDidWatch = Event.toPromise(watcher.onDidWatch); await Promises.mkdir(folderPath); await changeFuture; - await Event.toPromise(watcher.onDidWatch); + await onDidWatch; + + await timeout(500); // somehow needed on Linux await basicCrudTest(filePath, undefined, 1); }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index d01abbc9cf1..66cff804750 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -31,11 +31,13 @@ import { Emitter, Event } from 'vs/base/common/event'; class TestParcelWatcher extends ParcelWatcher { - protected override readonly missingRequestPathPollingInterval = 100; + protected override readonly suspendedWatchRequestPollingInterval = 100; private readonly _onDidWatch = this._register(new Emitter()); readonly onDidWatch = this._onDidWatch.event; + readonly onWatchFail = this._onDidWatchFail.event; + testNormalizePaths(paths: string[], excludes: string[] = []): string[] { // Work with strings as paths to simplify testing @@ -550,7 +552,7 @@ import { Emitter, Event } from 'vs/base/common/event'; await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]); }); - (isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path is handled properly', async function () { + (isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path without correlation restarts watching', async function () { const watchedPath = join(testDir, 'deep'); await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]); @@ -655,9 +657,31 @@ import { Emitter, Event } from 'vs/base/common/event'; await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3); }); + test('watching missing path emits watcher fail event', async function () { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'missing'); + watcher.watch([{ path: folderPath, excludes: [], recursive: true }]); + + await onDidWatchFail; + }); + + test('deleting watched path emits watcher fail event', async function () { + const folderPath = join(testDir, 'deep'); + + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + Promises.rm(folderPath, RimRafMode.UNLINK); + await onDidWatchFail; + }); + test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => { + let onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const folderPath = join(testDir, 'not-found'); await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + await onDidWatchFail; let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); let onDidWatch = Event.toPromise(watcher.onDidWatch); @@ -665,13 +689,12 @@ import { Emitter, Event } from 'vs/base/common/event'; await changeFuture; await onDidWatch; - await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, 1); - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); - onDidWatch = Event.toPromise(watcher.onDidWatch); + onDidWatchFail = Event.toPromise(watcher.onWatchFail); await Promises.rm(folderPath); - await changeFuture; - await onDidWatch; + await onDidWatchFail; changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); onDidWatch = Event.toPromise(watcher.onDidWatch); @@ -679,25 +702,26 @@ import { Emitter, Event } from 'vs/base/common/event'; await changeFuture; await onDidWatch; - await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + await basicCrudTest(filePath, 1); }); test('correlated watch requests support suspend/resume (folder, exist in beginning)', async () => { const folderPath = join(testDir, 'deep'); await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); - let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); - let onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.rm(folderPath); - await changeFuture; - await onDidWatch; + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, 1); - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); - onDidWatch = Event.toPromise(watcher.onDidWatch); + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; + + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + const onDidWatch = Event.toPromise(watcher.onDidWatch); await Promises.mkdir(folderPath); await changeFuture; await onDidWatch; - await basicCrudTest(join(folderPath, 'newFile.txt'), 1); + await basicCrudTest(filePath, 1); }); }); From 026f29a7b042a8c553231eee38be3ed5c16a84e9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 12 Mar 2024 11:40:46 +0100 Subject: [PATCH 138/141] Don't use suite setup and teardown in resolveExternal test (#207339) * Dispose instantiationService in resolveExternal test * Revert "Dispose instantiationService in resolveExternal test" This reverts commit 0fba9f401a9a059bfec6e3c33139424319b6b05c. * Don't use suite setup and teardown --- .../workbench/test/electron-sandbox/resolveExternal.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts index 9f7b6977176..14536883bea 100644 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -70,13 +70,13 @@ suite.skip('NativeWindow:resolveExternal', () => { const tunnelMock = new TunnelMock(); let window: TestNativeWindow; - suiteSetup(() => { + setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); instantiationService.stub(ITunnelService, tunnelMock); window = disposables.add(instantiationService.createInstance(TestNativeWindow)); }); - suiteTeardown(() => { + teardown(() => { disposables.clear(); }); From 7b43eb080007e46b099c0420db3c0f1f007b27e1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 12 Mar 2024 12:37:18 +0100 Subject: [PATCH 139/141] watcher - emit DELETE for root path deletions (#207408) --- .../files/node/watcher/baseWatcher.ts | 16 +++++------ .../node/watcher/nodejs/nodejsWatcherLib.ts | 28 +++++++++---------- .../node/watcher/parcel/parcelWatcher.ts | 17 +++++++---- .../node/nodejsWatcher.integrationTest.ts | 10 +++++-- .../node/parcelWatcher.integrationTest.ts | 4 ++- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 368da259162..34c9d6943d8 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -33,20 +33,20 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } private handleDidWatchFail(request: IUniversalWatchRequest): void { - if (!this.supportsRequestSuspendResume(request)) { + if (!this.isCorrelated(request)) { + + // For now, limit failed watch monitoring to requests with a correlationId + // to experiment with this feature in a controlled way. Monitoring requests + // requires us to install polling watchers (via `fs.watchFile()`) and thus + // should be used sparingly. + return; } this.suspendWatchRequest(request); } - protected supportsRequestSuspendResume(request: IUniversalWatchRequest): boolean { - - // For now, limit failed watch monitoring to requests with a correlationId - // to experiment with this feature in a controlled way. Monitoring requests - // requires us to install polling watchers (via `fs.watchFile()`) and thus - // should be used sparingly. - + protected isCorrelated(request: IUniversalWatchRequest): boolean { return typeof request.correlationId === 'number'; } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 82d4f43f58c..df2f4f031eb 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -224,13 +224,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { // file watching specifically we want to handle // the atomic-write cases where the file is being // deleted and recreated with different contents. - // - // Same as with recursive watching, we do not - // emit a delete event in this case. if (changedFileName === pathBasename && !await Promises.exists(path)) { - this.warn('Watcher shutdown because watched path got deleted'); - - this.onDidWatchFail?.(); + this.onWatchedPathDeleted(requestResource); return; } @@ -324,16 +319,9 @@ export class NodeJSFileWatcherLibrary extends Disposable { disposables.add(await this.doWatch(path, false)); } - // File seems to be really gone, so emit a deleted event and dispose + // File seems to be really gone, so emit a deleted and failed event else { - this.onFileChange({ resource: requestResource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); - - // Important to flush the event delivery - // before disposing the watcher, otherwise - // we will loose this event. - this.fileChangesAggregator.flush(); - - this.onDidWatchFail?.(); + this.onWatchedPathDeleted(requestResource); } }, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY); @@ -365,6 +353,16 @@ export class NodeJSFileWatcherLibrary extends Disposable { }); } + private onWatchedPathDeleted(resource: URI): void { + this.warn('Watcher shutdown because watched path got deleted'); + + // Emit events and flush in case the watcher gets disposed + this.onFileChange({ resource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); + this.fileChangesAggregator.flush(); + + this.onDidWatchFail?.(); + } + private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void { if (this.cts.token.isCancellationRequested) { return; diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 96096f35997..249c728e131 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -441,18 +441,25 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { let rootDeleted = false; for (const event of events) { - if (event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux)) { + rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux); + + if (rootDeleted && !this.isCorrelated(watcher.request)) { // Explicitly exclude changes to root if we have any // to avoid VS Code closing all opened editors which // can happen e.g. in case of network connectivity // issues // (https://github.com/microsoft/vscode/issues/136673) + // + // Update 2024: with the new correlated events, we + // really do not want to skip over file events any + // more, so we only ignore this event for non-correlated + // watch requests. - rootDeleted = true; - } else { - filteredEvents.push(event); + continue; } + + filteredEvents.push(event); } return { events: filteredEvents, rootDeleted }; @@ -470,7 +477,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // support in the super class so that we have 1 consistent // solution for handling this. - if (!this.supportsRequestSuspendResume(watcher.request)) { + if (!this.isCorrelated(watcher.request)) { this.legacyMonitorRequest(watcher); } } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 259b8a4e196..e2c73716a25 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -545,24 +545,28 @@ import { Emitter, Event } from 'vs/base/common/event'; await onDidWatchFail; }); - test('deleting watched path emits watcher fail event (file watch)', async function () { + test('deleting watched path emits watcher fail and delete event when correlated (file watch)', async function () { const filePath = join(testDir, 'lorem.txt'); - await watcher.watch([{ path: filePath, excludes: [], recursive: false }]); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); Promises.unlink(filePath); await onDidWatchFail; + await changeFuture; }); - (isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail event (folder watch)', async function () { + (isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () { const folderPath = join(testDir, 'deep'); await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); Promises.rm(folderPath, RimRafMode.UNLINK); await onDidWatchFail; + await changeFuture; }); test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () { diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 66cff804750..83854e006db 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -666,14 +666,16 @@ import { Emitter, Event } from 'vs/base/common/event'; await onDidWatchFail; }); - test('deleting watched path emits watcher fail event', async function () { + test('deleting watched path emits watcher fail and delete event if correlated', async function () { const folderPath = join(testDir, 'deep'); await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); Promises.rm(folderPath, RimRafMode.UNLINK); await onDidWatchFail; + await changeFuture; }); test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => { From be776fbfff16d18b19457cc271e4e049b766490d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 12 Mar 2024 13:04:27 +0100 Subject: [PATCH 140/141] "Retry as admin" don't respect "File: Save without Formatting" (fix #207401) (#207411) --- .../editors/textFileSaveErrorHandler.ts | 29 +++++++++++-------- .../textfile/common/textFileEditorModel.ts | 2 +- .../services/textfile/common/textfiles.ts | 2 +- .../common/storedFileWorkingCopy.ts | 14 ++++----- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 7d04ebe6d5d..066e0372c6b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -9,7 +9,7 @@ import { basename, isEqual } from 'vs/base/common/resources'; import { Action } from 'vs/base/common/actions'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IWriteFileOptions } from 'vs/platform/files/common/files'; -import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, ITextFileSaveAsOptions, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -99,7 +99,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } } - onSaveError(error: unknown, model: ITextFileEditorModel): void { + onSaveError(error: unknown, model: ITextFileEditorModel, options: ITextFileSaveOptions): void { const fileOperationError = error as FileOperationError; const resource = model.resource; @@ -127,7 +127,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", basename(resource)); primaryActions.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model)); - primaryActions.push(this.instantiationService.createInstance(SaveModelIgnoreModifiedSinceAction, model)); + primaryActions.push(this.instantiationService.createInstance(SaveModelIgnoreModifiedSinceAction, model, options)); secondaryActions.push(this.instantiationService.createInstance(ConfigureSaveConflictAction)); } @@ -142,17 +142,17 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // Save Elevated if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { - primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, !!triedToUnlock)); + primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, options, !!triedToUnlock)); } // Unlock else if (isWriteLocked) { - primaryActions.push(this.instantiationService.createInstance(UnlockModelAction, model)); + primaryActions.push(this.instantiationService.createInstance(UnlockModelAction, model, options)); } // Retry else { - primaryActions.push(this.instantiationService.createInstance(RetrySaveModelAction, model)); + primaryActions.push(this.instantiationService.createInstance(RetrySaveModelAction, model, options)); } // Save As @@ -272,6 +272,7 @@ class SaveModelElevatedAction extends Action { constructor( private model: ITextFileEditorModel, + private options: ITextFileSaveOptions, private triedToUnlock: boolean ) { super('workbench.files.action.saveModelElevated', triedToUnlock ? isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo...")); @@ -280,6 +281,7 @@ class SaveModelElevatedAction extends Action { override async run(): Promise { if (!this.model.isDisposed()) { await this.model.save({ + ...this.options, writeElevated: true, writeUnlock: this.triedToUnlock, reason: SaveReason.EXPLICIT @@ -291,14 +293,15 @@ class SaveModelElevatedAction extends Action { class RetrySaveModelAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.saveModel', localize('retry', "Retry")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, reason: SaveReason.EXPLICIT }); } } } @@ -360,14 +363,15 @@ class SaveModelAsAction extends Action { class UnlockModelAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.unlock', localize('overwrite', "Overwrite")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ writeUnlock: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, writeUnlock: true, reason: SaveReason.EXPLICIT }); } } } @@ -375,14 +379,15 @@ class UnlockModelAction extends Action { class SaveModelIgnoreModifiedSinceAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.saveIgnoreModifiedSince', localize('overwrite', "Overwrite")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 4fb09ccd015..b127ae7b7ca 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -963,7 +963,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Show to user - this.textFileService.files.saveErrorHandler.onSaveError(error, this); + this.textFileService.files.saveErrorHandler.onSaveError(error, this, options); // Emit as event this._onDidSaveError.fire(); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 306bca1e19c..d126c88df6e 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -195,7 +195,7 @@ export interface ISaveErrorHandler { /** * Called whenever a save fails. */ - onSaveError(error: Error, model: ITextFileEditorModel): void; + onSaveError(error: Error, model: ITextFileEditorModel, options: ITextFileSaveAsOptions): void; } /** diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 7da85b4899c..979d47aa8ea 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -1100,13 +1100,13 @@ export class StoredFileWorkingCopy extend } // Show save error to user for handling - this.doHandleSaveError(error); + this.doHandleSaveError(error, options); // Emit as event this._onDidSaveError.fire(); } - private doHandleSaveError(error: Error): void { + private doHandleSaveError(error: Error, options: IStoredFileWorkingCopySaveAsOptions): void { const fileOperationError = error as FileOperationError; const primaryActions: IAction[] = []; @@ -1116,7 +1116,7 @@ export class StoredFileWorkingCopy extend if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Do you want to overwrite the file with your changes?", this.name); - primaryActions.push(toAction({ id: 'fileWorkingCopy.overwrite', label: localize('overwrite', "Overwrite"), run: () => this.save({ ignoreModifiedSince: true }) })); + primaryActions.push(toAction({ id: 'fileWorkingCopy.overwrite', label: localize('overwrite', "Overwrite"), run: () => this.save({ ...options, ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }) })); primaryActions.push(toAction({ id: 'fileWorkingCopy.revert', label: localize('discard', "Discard"), run: () => this.revert() })); } @@ -1140,19 +1140,19 @@ export class StoredFileWorkingCopy extend isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo..."), run: () => { - this.save({ writeElevated: true, writeUnlock: triedToUnlock, reason: SaveReason.EXPLICIT }); + this.save({ ...options, writeElevated: true, writeUnlock: triedToUnlock, reason: SaveReason.EXPLICIT }); } })); } // Unlock else if (isWriteLocked) { - primaryActions.push(toAction({ id: 'fileWorkingCopy.unlock', label: localize('overwrite', "Overwrite"), run: () => this.save({ writeUnlock: true, reason: SaveReason.EXPLICIT }) })); + primaryActions.push(toAction({ id: 'fileWorkingCopy.unlock', label: localize('overwrite', "Overwrite"), run: () => this.save({ ...options, writeUnlock: true, reason: SaveReason.EXPLICIT }) })); } // Retry else { - primaryActions.push(toAction({ id: 'fileWorkingCopy.retry', label: localize('retry', "Retry"), run: () => this.save({ reason: SaveReason.EXPLICIT }) })); + primaryActions.push(toAction({ id: 'fileWorkingCopy.retry', label: localize('retry', "Retry"), run: () => this.save({ ...options, reason: SaveReason.EXPLICIT }) })); } // Save As @@ -1164,7 +1164,7 @@ export class StoredFileWorkingCopy extend if (editor) { const result = await this.editorService.save(editor, { saveAs: true, reason: SaveReason.EXPLICIT }); if (!result.success) { - this.doHandleSaveError(error); // show error again given the operation failed + this.doHandleSaveError(error, options); // show error again given the operation failed } } } From 74020cb0ccb99ec466d342f0906bd8984946b805 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 12 Mar 2024 14:24:00 +0100 Subject: [PATCH 141/141] Inline chat content overlay (#207415) * add inline chat content widget for first interaction * better layout of the inline chat content widget * fix alignment of toolbars when having multiline inputs * set width of progress bar * more size tweaking --- .../contrib/inlineChat/browser/inlineChat.css | 28 ++--- .../browser/inlineChatContentWidget.css | 11 ++ .../browser/inlineChatContentWidget.ts | 119 ++++++++++++++++++ .../browser/inlineChatController.ts | 31 +++-- .../browser/inlineChatInputWidget.ts | 21 +++- .../inlineChat/browser/inlineChatWidget.ts | 15 ++- .../browser/inlineChatZoneWidget.ts | 30 ++--- 7 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 1b5ea26167f..acab29a82f4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -26,19 +26,20 @@ display: flex; } -.monaco-workbench .inline-chat .body .content { +.monaco-workbench .inline-chat-input { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); outline-offset: -1px; border-radius: 2px; + background-color: var(--vscode-inlineChatInput-background); } -.monaco-workbench .inline-chat .body .content.synthetic-focus { +.monaco-workbench .inline-chat-input.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.monaco-workbench .inline-chat .body .content .input { +.monaco-workbench .inline-chat-input .input { display: flex; align-items: center; justify-content: space-between; @@ -47,11 +48,11 @@ cursor: text; } -.monaco-workbench .inline-chat .body .content .input .monaco-editor-background { +.monaco-workbench .inline-chat-input .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.monaco-workbench .inline-chat .body .content .input .editor-placeholder { +.monaco-workbench .inline-chat-input .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -60,25 +61,23 @@ text-overflow: ellipsis; } -.monaco-workbench .inline-chat .body .content .input .editor-placeholder.hidden { +.monaco-workbench .inline-chat-input .input .editor-placeholder.hidden { display: none; } -.monaco-workbench .inline-chat .body .content .input .editor-container { +.monaco-workbench .inline-chat-input .input .editor-container { vertical-align: middle; } -.monaco-workbench .inline-chat .body .toolbar { +.monaco-workbench .inline-chat-input .toolbar { display: flex; flex-direction: column; - align-self: stretch; + align-self: flex-start; + padding-top: 4px; padding-right: 4px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - background: var(--vscode-inlineChatInput-background); } -.monaco-workbench .inline-chat .body .toolbar .actions-container { +.monaco-workbench .inline-chat-input .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; @@ -86,14 +85,13 @@ .monaco-workbench .inline-chat .body > .widget-toolbar { padding-left: 4px; + align-self: flex-start; } /* progress bit */ .monaco-workbench .inline-chat .progress { position: relative; - width: calc(100% - 18px); - left: 19px; } /* UGLY - fighting against workbench styles */ diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css new file mode 100644 index 00000000000..011978066d5 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .inline-chat-content-widget { + padding: 6px; + border-radius: 4px; + background-color: var(--vscode-inlineChat-background); + box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts new file mode 100644 index 00000000000..1462e6066db --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./inlineChatContentWidget'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import * as dom from 'vs/base/browser/dom'; +import { IDimension } from 'vs/editor/common/core/dimension'; +import { Emitter, Event } from 'vs/base/common/event'; +import { InlineChatInputWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { clamp } from 'vs/base/common/numbers'; + +export class InlineChatContentWidget implements IContentWidget { + + readonly suppressMouseDown = false; + readonly allowEditorOverflow = true; + + private readonly _store = new DisposableStore(); + private readonly _domNode = document.createElement('div'); + + private _position?: IPosition; + + private readonly _onDidBlur = this._store.add(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _visible: boolean = false; + private _focusNext: boolean = false; + + + constructor( + private readonly _editor: ICodeEditor, + private readonly _widget: InlineChatInputWidget, + ) { + this._store.add(this._widget.onDidChangeHeight(() => _editor.layoutContentWidget(this))); + + // this._store.add(dom.addStandardDisposableListener(this._domNode, 'click', _e => { this._widget.focus(); })); + + this._domNode.tabIndex = -1; + this._domNode.className = 'inline-chat-content-widget'; + this._domNode.appendChild(this._widget.domNode); + + const tracker = dom.trackFocus(this._domNode); + this._store.add(tracker.onDidBlur(() => { + if (this._visible) { + this._onDidBlur.fire(); + } + })); + this._store.add(tracker); + } + + dispose(): void { + this._store.dispose(); + } + + getId(): string { + return 'inline-chat-content-widget'; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IContentWidgetPosition | null { + if (!this._position) { + return null; + } + return { + position: this._position, + preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW] + }; + } + + beforeRender(): IDimension | null { + + const contentWidth = this._editor.getLayoutInfo().contentWidth; + const minWidth = contentWidth * 0.33; + const maxWidth = contentWidth * 0.66; + + const dim = this._widget.getPreferredSize(); + const width = clamp(dim.width, minWidth, maxWidth); + this._widget.layout(new dom.Dimension(width, dim.height)); + + return null; + } + + afterRender(): void { + if (this._focusNext) { + this._focusNext = false; + this._widget.focus(); + } + } + + // --- + + show(position: IPosition) { + if (!this._visible) { + this._visible = true; + this._focusNext = true; + + this._widget.moveTo(this._domNode); + this._widget.reset(); + + const wordInfo = this._editor.getModel()?.getWordAtPosition(position); + + this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; + this._editor.addContentWidget(this); + } + } + + hide() { + if (this._visible) { + this._visible = false; + this._editor.removeContentWidget(this); + } + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index e2feff8c52f..5fc119fa1bb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -44,10 +44,11 @@ import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; -import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; import { IValidEditOperation } from 'vs/editor/common/model'; +import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -114,7 +115,10 @@ export class InlineChatController implements IEditorContribution { private _isDisposed: boolean = false; private readonly _store = new DisposableStore(); + private readonly _input: Lazy; private readonly _zone: Lazy; + + private readonly _ctxVisible: IContextKey; private readonly _ctxHasActiveRequest: IContextKey; private readonly _ctxResponseTypes: IContextKey; private readonly _ctxDidEdit: IContextKey; @@ -151,13 +155,16 @@ export class InlineChatController implements IEditorContribution { @IStorageService private readonly _storageService: IStorageService, @ICommandService private readonly _commandService: ICommandService, ) { + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService); this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); this._ctxResponseTypes = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(contextKeyService); this._ctxLastFeedbackKind = CTX_INLINE_CHAT_LAST_FEEDBACK.bindTo(contextKeyService); this._ctxSupportIssueReporting = CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING.bindTo(contextKeyService); + this._zone = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatZoneWidget, this._editor))); + this._input = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatContentWidget, this._editor, this._zone.value.widget.inputWidget))); this._store.add(this._editor.onDidChangeModel(async e => { if (this._session || !e.newModelUrl) { @@ -377,6 +384,8 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.add(this._session.wholeRange.onDidChange(updateWholeRangeDecoration)); updateWholeRangeDecoration(); + this._sessionStore.add(this._input.value.onDidBlur(() => this.cancelSession())); + this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); @@ -548,6 +557,8 @@ export class InlineChatController implements IEditorContribution { assertType(this._strategy); assertType(this._session.lastInput); + this._showWidget(false); + const requestCts = new CancellationTokenSource(); let message = Message.NONE; @@ -912,32 +923,36 @@ export class InlineChatController implements IEditorContribution { widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); } - if (initialRender) { - this._zone.value.setContainerMargins(); - } - if (this._session && !position && (this._session.hasChangedText || this._session.lastExchange)) { widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); } if (this._session) { this._zone.value.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); } + if (!this._zone.value.position) { - this._zone.value.setWidgetMargins(widgetPosition); - this._zone.value.show(widgetPosition); + if (initialRender) { + // this._zone.value.hide(); + this._input.value.show(this._editor.getSelection().getStartPosition()); + } else { + this._input.value.hide(); + this._zone.value.show(widgetPosition); + } } else { - this._zone.value.setWidgetMargins(widgetPosition); this._zone.value.updatePositionAndHeight(widgetPosition); } + this._ctxVisible.set(true); } private _resetWidget() { this._sessionStore.clear(); + this._ctxVisible.reset(); this._ctxDidEdit.reset(); this._ctxUserDidEdit.reset(); this._ctxLastFeedbackKind.reset(); this._ctxSupportIssueReporting.reset(); + this._input.rawValue?.hide(); this._zone.rawValue?.hide(); // Return focus to the editor only if the current focus is within the editor widget diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts index f32ce39084e..629c3cf97bd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Dimension, addDisposableListener, getTotalWidth, h } from 'vs/base/browser/dom'; +import { Dimension, addDisposableListener, getTotalWidth, h, isAncestor } from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -37,7 +37,7 @@ import { localize } from 'vs/nls'; export class InlineChatInputWidget { private readonly _elements = h( - 'div.content@content', + 'div.inline-chat-input@content', [ h('div.input@input', [ h('div.editor-placeholder@placeholder'), @@ -55,7 +55,6 @@ export class InlineChatInputWidget { private readonly _ctxInnerCursorStart: IContextKey; private readonly _ctxInnerCursorEnd: IContextKey; private readonly _ctxInputEditorFocused: IContextKey; - // private readonly _ctxResponseFocused: IContextKey; private readonly _inputEditor: IActiveCodeEditor; private readonly _inputModel: ITextModel; @@ -137,6 +136,7 @@ export class InlineChatInputWidget { this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); }; this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); + this._store.add(this._inputEditor.onDidChangeModelContent(updateInnerCursorFirstLast)); updateInnerCursorFirstLast(); // (2) input editor focused or not @@ -183,6 +183,12 @@ export class InlineChatInputWidget { return this._elements.content; } + moveTo(parent: HTMLElement) { + if (!isAncestor(this.domNode, parent)) { + parent.insertBefore(this.domNode, parent.firstChild); + } + } + layout(dim: Dimension) { const toolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; const editorWidth = dim.width - toolbarWidth; @@ -190,8 +196,10 @@ export class InlineChatInputWidget { this._elements.placeholder.style.width = `${editorWidth}px`; } - getPreferredHeight(): number { - return this._inputEditor.getContentHeight(); + getPreferredSize(): Dimension { + const width = this._inputEditor.getContentWidth() + getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; + const height = this._inputEditor.getContentHeight(); + return new Dimension(width, height); } reset() { @@ -201,7 +209,8 @@ export class InlineChatInputWidget { this._ctxInnerCursorStart.reset(); this._ctxInnerCursorEnd.reset(); this._ctxInputEditorFocused.reset(); - this.value = ''; + + this.value = ''; // update/re-inits some context keys again } focus() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 77325ccd8c2..1e5aef48263 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -188,7 +188,7 @@ export class InlineChatWidget { // input editor logic this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: options.inputMenuId, telemetrySource: options.telemetrySource, hoverDelegate }); - this._elements.body.replaceChild(this._inputWidget.domNode, this._elements.content); + this._inputWidget.moveTo(this._elements.content); this._store.add(this._inputWidget); this._store.add(this._inputWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); @@ -289,7 +289,7 @@ export class InlineChatWidget { try { const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); const innerEditorWidth = widgetDim.width - widgetToolbarWidth; - const inputDim = new Dimension(innerEditorWidth, this._inputWidget.getPreferredHeight()); + const inputDim = new Dimension(innerEditorWidth, this._inputWidget.getPreferredSize().height); if (!this._lastDim || !Dimension.equals(this._lastDim, inputDim)) { this._lastDim = inputDim; this._doLayout(widgetDim, inputDim); @@ -301,6 +301,7 @@ export class InlineChatWidget { } protected _doLayout(widgetDimension: Dimension, inputDimension: Dimension): void { + this._elements.progress.style.width = `${inputDimension.width}px`; this._chatMessageContents.style.width = `${widgetDimension.width - 10}px`; this._chatMessageContents.style.maxHeight = `270px`; @@ -308,7 +309,7 @@ export class InlineChatWidget { } getHeight(): number { - const editorHeight = this._inputWidget.getPreferredHeight() + 4 /*padding*/; + const editorHeight = this._inputWidget.getPreferredSize().height + 4 /*padding*/; const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); const chatResponseHeight = getTotalHeight(this._chatMessageContents); @@ -327,6 +328,14 @@ export class InlineChatWidget { } } + get inputWidget(): InlineChatInputWidget { + return this._inputWidget; + } + + takeInputWidgetOwnership(): void { + this._inputWidget.moveTo(this._elements.content); + } + get value(): string { return this._inputWidget.value; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 6a23b3de343..6e26929cb62 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -14,7 +14,7 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; @@ -22,7 +22,6 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; - private readonly _ctxVisible: IContextKey; private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; private _indentationWidth: number | undefined; @@ -34,11 +33,9 @@ export class InlineChatZoneWidget extends ZoneWidget { ) { super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); this._disposables.add(toDisposable(() => { - this._ctxVisible.reset(); this._ctxCursorPosition.reset(); })); @@ -128,9 +125,21 @@ export class InlineChatZoneWidget extends ZoneWidget { } override show(position: Position): void { + assertType(this.container); + + const info = this.editor.getLayoutInfo(); + const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; + this.container.style.marginLeft = `${marginWithoutIndentation}px`; + + this._setWidgetMargins(position); + this.widget.takeInputWidgetOwnership(); super.show(position, this._computeHeightInLines()); this.widget.focus(); - this._ctxVisible.set(true); + } + + override updatePositionAndHeight(position: Position): void { + this._setWidgetMargins(position); + super.updatePositionAndHeight(position); } protected override _getWidth(info: EditorLayoutInfo): number { @@ -164,15 +173,7 @@ export class InlineChatZoneWidget extends ZoneWidget { return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); } - setContainerMargins(): void { - assertType(this.container); - - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - } - - setWidgetMargins(position: Position): void { + private _setWidgetMargins(position: Position): void { const indentationWidth = this._calculateIndentationWidth(position); if (this._indentationWidth === indentationWidth) { return; @@ -184,7 +185,6 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { this.container!.classList.remove('inside-selection'); - this._ctxVisible.reset(); this._ctxCursorPosition.reset(); this.widget.reset(); super.hide();