Merge pull request #29 from quant-eagle/fix/codelens_imports

fix: update VS source to more recent version
This commit is contained in:
Mathew Pareles 2024-09-19 10:59:46 -07:00 committed by GitHub
commit d3fd53974e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 1032 additions and 575 deletions

View file

@ -136,6 +136,6 @@ export function isWCOEnabled(): boolean {
// Returns the bounding rect of the titlebar area if it is supported and defined // Returns the bounding rect of the titlebar area if it is supported and defined
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect // See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
export function getWCOBoundingRect(): DOMRect | undefined { export function getWCOTitlebarAreaRect(targetWindow: Window): DOMRect | undefined {
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect(); return (targetWindow.navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
} }

View file

@ -11,7 +11,7 @@ import { $ } from 'vs/base/browser/dom';
import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate';
import { Button } from 'vs/base/browser/ui/button/button'; import { Button } from 'vs/base/browser/ui/button/button';
import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle';
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
export interface IRadioStyles { export interface IRadioStyles {
readonly activeForeground?: string; readonly activeForeground?: string;
@ -53,7 +53,7 @@ export class Radio extends Widget {
constructor(opts: IRadioOptions) { constructor(opts: IRadioOptions) {
super(); super();
this.hoverDelegate = opts.hoverDelegate ?? getDefaultHoverDelegate('element'); this.hoverDelegate = opts.hoverDelegate ?? this._register(createInstantHoverDelegate());
this.domNode = $('.monaco-custom-radio'); this.domNode = $('.monaco-custom-radio');
this.domNode.setAttribute('role', 'radio'); this.domNode.setAttribute('role', 'radio');

View file

@ -217,24 +217,6 @@ export interface FileFilter {
name: string; name: string;
} }
export interface OpenDevToolsOptions {
/**
* Opens the devtools with specified dock state, can be `left`, `right`, `bottom`,
* `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's
* possible to dock back. In `detach` mode it's not.
*/
mode: ('left' | 'right' | 'bottom' | 'undocked' | 'detach');
/**
* Whether to bring the opened devtools window to the foreground. The default is
* `true`.
*/
activate?: boolean;
/**
* A title for the DevTools window (only in `undocked` or `detach` mode).
*/
title?: string;
}
interface InputEvent { interface InputEvent {
// Docs: https://electronjs.org/docs/api/structures/input-event // Docs: https://electronjs.org/docs/api/structures/input-event

View file

@ -222,7 +222,7 @@ export class HoverWidget extends Widget implements IHoverWidget {
} }
// Show the hover hint if needed // Show the hover hint if needed
if (hideOnHover && options.appearance?.showHoverHint) { if (options.appearance?.showHoverHint) {
const statusBarElement = $('div.hover-row.status-bar'); const statusBarElement = $('div.hover-row.status-bar');
const infoElement = $('div.info'); const infoElement = $('div.info');
infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt'); infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');

View file

@ -2019,9 +2019,9 @@ export interface CommentThreadChangedEvent<T> {
} }
export interface CodeLens { export interface CodeLens {
range: IRange; // the range of code range: IRange;
id?: string; // no idea what this is for id?: string;
command?: Command; // command to run when they click the codeLens command?: Command;
} }
export interface CodeLensList { export interface CodeLensList {

View file

@ -180,6 +180,12 @@ export class CodeActionController extends Disposable implements IEditorContribut
return; return;
} }
const selection = this._editor.getSelection();
if (selection?.startLineNumber !== newState.position.lineNumber) {
return;
}
this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position); this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position);
if (newState.trigger.type === CodeActionTriggerType.Invoke) { if (newState.trigger.type === CodeActionTriggerType.Invoke) {

View file

@ -75,6 +75,14 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
private _gutterState: LightBulbState.State = LightBulbState.Hidden; private _gutterState: LightBulbState.State = LightBulbState.Hidden;
private _iconClasses: string[] = []; private _iconClasses: string[] = [];
private readonly lightbulbClasses = [
'codicon-' + GUTTER_LIGHTBULB_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id,
'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id
];
private _preferredKbLabel?: string; private _preferredKbLabel?: string;
private _quickFixKbLabel?: string; private _quickFixKbLabel?: string;
@ -148,15 +156,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
})); }));
this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => {
const lightbulbClasses = [
'codicon-' + GUTTER_LIGHTBULB_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id,
'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id
];
if (!e.target.element || !lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { if (!e.target.element || !this.lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) {
return; return;
} }
@ -247,7 +248,9 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
let hasDecoration = false; let hasDecoration = false;
if (currLineDecorations) { if (currLineDecorations) {
for (const decoration of currLineDecorations) { for (const decoration of currLineDecorations) {
if (decoration.options.glyphMarginClassName) { const glyphClass = decoration.options.glyphMarginClassName;
if (glyphClass && !this.lightbulbClasses.some(className => glyphClass.includes(className))) {
hasDecoration = true; hasDecoration = true;
break; break;
} }
@ -271,7 +274,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber);
const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented;
// check above and below. if both are blocked, display lightbulb in the gutter. // check above and below. if both are blocked, display lightbulb in the gutter.
if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) {
this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, {
@ -280,7 +282,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
}); });
this.renderGutterLightbub(); this.renderGutterLightbub();
return this.hide(); return this.hide();
} else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { } else if (prevLineEmptyOrIndented || endLine || (prevLineEmptyOrIndented && !currLineEmptyOrIndented)) {
effectiveLineNumber -= 1; effectiveLineNumber -= 1;
} else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) {
effectiveLineNumber += 1; effectiveLineNumber += 1;

View file

@ -3,17 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Event } from '../../../../base/common/event.js'; import { Event } from 'vs/base/common/event';
import { LRUCache } from '../../../../base/common/map.js'; import { LRUCache } from 'vs/base/common/map';
import { Range } from '../../../common/core/range.js'; import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from '../../../common/model.js'; import { ITextModel } from 'vs/editor/common/model';
import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js'; import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages';
import { CodeLensModel } from './codelens.js'; import { CodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js'; import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { mainWindow } from '../../../../base/browser/window.js'; import { mainWindow } from 'vs/base/browser/window';
import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; import { runWhenWindowIdle } from 'vs/base/browser/dom';
export const ICodeLensCache = createDecorator<ICodeLensCache>('ICodeLensCache'); export const ICodeLensCache = createDecorator<ICodeLensCache>('ICodeLensCache');

View file

@ -3,17 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CancellationToken } from 'vs/base/common/cancellation';
import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { assertType } from '../../../../base/common/types.js'; import { assertType } from 'vs/base/common/types';
import { URI } from '../../../../base/common/uri.js'; import { URI } from 'vs/base/common/uri';
import { ITextModel } from '../../../common/model.js'; import { ITextModel } from 'vs/editor/common/model';
import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js'; import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages';
import { IModelService } from '../../../common/services/model.js'; import { IModelService } from 'vs/editor/common/services/model';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
export interface CodeLensItem { export interface CodeLensItem {
symbol: CodeLens; symbol: CodeLens;

View file

@ -4,26 +4,26 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js'; import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from 'vs/base/common/async';
import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from '../../../browser/editorBrowser.js'; import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js'; import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorOption, EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js'; import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { IEditorContribution } from '../../../common/editorCommon.js'; import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IModelDecorationsChangeAccessor } from '../../../common/model.js'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
import { CodeLens, Command } from '../../../common/languages.js'; import { CodeLens, Command } from 'vs/editor/common/languages';
import { CodeLensItem, CodeLensModel, getCodeLensModel } from './codelens.js'; import { CodeLensItem, CodeLensModel, getCodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens';
import { ICodeLensCache } from './codeLensCache.js'; import { ICodeLensCache } from 'vs/editor/contrib/codelens/browser/codeLensCache';
import { CodeLensHelper, CodeLensWidget } from './codelensWidget.js'; import { CodeLensHelper, CodeLensWidget } from 'vs/editor/contrib/codelens/browser/codelensWidget';
import { localize } from '../../../../nls.js'; import { localize } from 'vs/nls';
import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ICommandService } from 'vs/platform/commands/common/commands';
import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
export class CodeLensContribution implements IEditorContribution { export class CodeLensContribution implements IEditorContribution {

View file

@ -3,16 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../base/browser/dom.js'; import * as dom from 'vs/base/browser/dom';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Constants } from '../../../../base/common/uint.js'; import { Constants } from 'vs/base/common/uint';
import './codelensWidget.css'; import 'vs/css!./codelensWidget';
import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from '../../../browser/editorBrowser.js'; import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { Range } from '../../../common/core/range.js'; import { Range } from 'vs/editor/common/core/range';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from '../../../common/model.js'; import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { CodeLens, Command } from '../../../common/languages.js'; import { CodeLens, Command } from 'vs/editor/common/languages';
import { CodeLensItem } from './codelens.js'; import { CodeLensItem } from 'vs/editor/contrib/codelens/browser/codelens';
class CodeLensViewZone implements IViewZone { class CodeLensViewZone implements IViewZone {

View file

@ -318,7 +318,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable
} }
afterRender(position: ContentWidgetPositionPreference | null): void { afterRender(position: ContentWidgetPositionPreference | null): void {
this._trace('invoking afterRender, position: ', position ? 'not null' : 'null'); // FIXME@ulugbekna: commenting trace log out until we start unmounting the widget from editor properly - https://github.com/microsoft/vscode/issues/226975
// this._trace('invoking afterRender, position: ', position ? 'not null' : 'null');
if (position === null) { if (position === null) {
// cancel rename when input widget isn't rendered anymore // cancel rename when input widget isn't rendered anymore
this.cancelInput(true, 'afterRender (because position is null)'); this.cancelInput(true, 'afterRender (because position is null)');
@ -363,7 +364,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable
} }
cancelInput(focusEditor: boolean, caller: string): void { cancelInput(focusEditor: boolean, caller: string): void {
this._trace(`invoking cancelInput, caller: ${caller}, _currentCancelInput: ${this._currentAcceptInput ? 'not undefined' : 'undefined'}`); // this._trace(`invoking cancelInput, caller: ${caller}, _currentCancelInput: ${this._currentAcceptInput ? 'not undefined' : 'undefined'}`);
this._currentCancelInput?.(focusEditor); this._currentCancelInput?.(focusEditor);
} }

View file

@ -166,8 +166,8 @@ export class ActionList<T> extends Disposable {
private readonly _list: List<IActionListItem<T>>; private readonly _list: List<IActionListItem<T>>;
private readonly _actionLineHeight = 28; private readonly _actionLineHeight = 24;
private readonly _headerLineHeight = 28; private readonly _headerLineHeight = 26;
private readonly _allMenuItems: readonly IActionListItem<T>[]; private readonly _allMenuItems: readonly IActionListItem<T>[];

View file

@ -132,8 +132,9 @@
/* Action bar */ /* Action bar */
.action-widget .action-widget-action-bar { .action-widget .action-widget-action-bar {
background-color: var(--vscode-editorHoverWidget-statusBarBackground); background-color: var(--vscode-editorActionList-background);
border-top: 1px solid var(--vscode-editorHoverWidget-border); border-top: 1px solid var(--vscode-editorHoverWidget-border);
margin-top: 2px;
} }
.action-widget .action-widget-action-bar::before { .action-widget .action-widget-action-bar::before {
@ -143,7 +144,7 @@
} }
.action-widget .action-widget-action-bar .actions-container { .action-widget .action-widget-action-bar .actions-container {
padding: 0 8px; padding: 3px 8px 0;
} }
.action-widget-action-bar .action-label { .action-widget-action-bar .action-label {

View file

@ -16,15 +16,15 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>(); private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>();
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }> = this._onDidChangeEnablement.event; readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }> = this._onDidChangeEnablement.event;
private readonly storageManger: StorageManager; private readonly storageManager: StorageManager;
constructor( constructor(
@IStorageService storageService: IStorageService, @IStorageService storageService: IStorageService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService, @IExtensionManagementService extensionManagementService: IExtensionManagementService,
) { ) {
super(); super();
this.storageManger = this._register(new StorageManager(storageService)); this.storageManager = this._register(new StorageManager(storageService));
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' }))); this._register(this.storageManager.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => { this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => {
if (local && operation === InstallOperation.Migrate) { if (local && operation === InstallOperation.Migrate) {
this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */ this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */
@ -84,11 +84,11 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
} }
private _getExtensions(storageId: string): IExtensionIdentifier[] { private _getExtensions(storageId: string): IExtensionIdentifier[] {
return this.storageManger.get(storageId, StorageScope.PROFILE); return this.storageManager.get(storageId, StorageScope.PROFILE);
} }
private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void { private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void {
this.storageManger.set(storageId, extensions, StorageScope.PROFILE); this.storageManager.set(storageId, extensions, StorageScope.PROFILE);
} }
} }

View file

@ -6,7 +6,7 @@
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes'; import { MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes';
import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { ISerializableCommandAction } from 'vs/platform/action/common/action';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@ -178,7 +178,7 @@ export interface ICommonNativeHostService {
exit(code: number): Promise<void>; exit(code: number): Promise<void>;
// Development // Development
openDevTools(options?: Partial<OpenDevToolsOptions> & INativeHostOptions): Promise<void>; openDevTools(options?: INativeHostOptions): Promise<void>;
toggleDevTools(options?: INativeHostOptions): Promise<void>; toggleDevTools(options?: INativeHostOptions): Promise<void>;
// Perf Introspection // Perf Introspection

View file

@ -5,7 +5,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron';
import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os'; import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os';
import { promisify } from 'util'; import { promisify } from 'util';
import { memoize } from 'vs/base/common/decorators'; import { memoize } from 'vs/base/common/decorators';
@ -33,7 +33,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IPartsSplash } from 'vs/platform/theme/common/themeService';
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
import { ICodeWindow } from 'vs/platform/window/electron-main/window'; import { ICodeWindow } from 'vs/platform/window/electron-main/window';
import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable, useWindowControlsOverlay } from 'vs/platform/window/common/window';
import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
@ -855,14 +855,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#region Development //#region Development
async openDevTools(windowId: number | undefined, options?: Partial<OpenDevToolsOptions> & INativeHostOptions): Promise<void> { async openDevTools(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId); const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.webContents.openDevTools(options?.mode ? { mode: options.mode, activate: options.activate } : undefined);
let mode: 'bottom' | undefined = undefined;
if (isLinux && useWindowControlsOverlay(this.configurationService)) {
mode = 'bottom'; // TODO@bpasero WCO and devtools collide with default option 'right'
}
window?.win?.webContents.openDevTools(mode ? { mode } : undefined);
} }
async toggleDevTools(windowId: number | undefined, options?: INativeHostOptions): Promise<void> { async toggleDevTools(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId); const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.webContents.toggleDevTools(); const webContents = window?.win?.webContents;
if (!webContents) {
return;
}
if (isLinux && useWindowControlsOverlay(this.configurationService) && !webContents.isDevToolsOpened()) {
webContents.openDevTools({ mode: 'bottom' }); // TODO@bpasero WCO and devtools collide with default option 'right'
} else {
webContents.toggleDevTools();
}
} }
//#endregion //#endregion

View file

@ -159,7 +159,7 @@ class ColorRegistry implements IColorRegistry {
public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier {
const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage };
this.colorsById[id] = colorContribution; this.colorsById[id] = colorContribution;
const propertySchema: IJSONSchemaWithSnippets = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; const propertySchema: IJSONSchemaWithSnippets = { type: 'string', format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] };
if (deprecationMessage) { if (deprecationMessage) {
propertySchema.deprecationMessage = deprecationMessage; propertySchema.deprecationMessage = deprecationMessage;
} }
@ -168,6 +168,7 @@ class ColorRegistry implements IColorRegistry {
propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content'); propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content');
} }
this.colorSchema.properties[id] = { this.colorSchema.properties[id] = {
description,
oneOf: [ oneOf: [
propertySchema, propertySchema,
{ type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') } { type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') }

View file

@ -14,7 +14,6 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { FileType } from 'vs/platform/files/common/files'; import { FileType } from 'vs/platform/files/common/files';
import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log'; import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log';
import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy';
import product from 'vs/platform/product/common/product';
import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IPartsSplash } from 'vs/platform/theme/common/themeService';
import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
@ -240,8 +239,6 @@ export function useWindowControlsOverlay(configurationService: IConfigurationSer
if (typeof setting === 'boolean') { if (typeof setting === 'boolean') {
return setting; return setting;
} }
return product.quality !== 'stable'; // disable by default in stable for now (TODO@bpasero TODO@benibenj flip when custom title is default)
} }
// Default to true. // Default to true.

View file

@ -28,6 +28,7 @@ import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSe
import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor';
import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays';
import { ICanonicalUriService } from 'vs/platform/workspace/common/canonicalUri'; import { ICanonicalUriService } from 'vs/platform/workspace/common/canonicalUri';
import { revive } from 'vs/base/common/marshalling';
@extHostNamedCustomer(MainContext.MainThreadWorkspace) @extHostNamedCustomer(MainContext.MainThreadWorkspace)
export class MainThreadWorkspace implements MainThreadWorkspaceShape { export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@ -146,7 +147,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
const query = this._queryBuilder.file( const query = this._queryBuilder.file(
includeFolder ? [includeFolder] : workspace.folders, includeFolder ? [includeFolder] : workspace.folders,
options revive(options)
); );
return this._searchService.fileSearch(query, token).then(result => { return this._searchService.fileSearch(query, token).then(result => {
@ -164,7 +165,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
const workspace = this._contextService.getWorkspace(); const workspace = this._contextService.getWorkspace();
const folders = folder ? [folder] : workspace.folders.map(folder => folder.uri); const folders = folder ? [folder] : workspace.folders.map(folder => folder.uri);
const query = this._queryBuilder.text(pattern, folders, options); const query = this._queryBuilder.text(pattern, folders, revive(options));
query._reason = 'startTextSearch'; query._reason = 'startTextSearch';
const onProgress = (p: ISearchProgressItem) => { const onProgress = (p: ISearchProgressItem) => {

View file

@ -550,14 +550,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider); return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
}, },
// VOID added this (I think will need to add this back when add ctrl+K)
// registerVoidCtrlKProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
// return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
// },
registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable {
return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider); return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider);
}, },

View file

@ -304,17 +304,6 @@ const newCommands: ApiCommand[] = [
})(value); })(value);
}) })
), ),
// // --- Void code lens
// new ApiCommand(
// 'vscode.executeVoidCodeLensProvider', '_executeVoidCodeLensProvider', 'Execute Void code lens provider.',
// [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('itemResolveCount', 'Number of lenses that should be resolved and returned. Will only return resolved lenses, will impact performance)').optional()],
// new ApiCommandResult<languages.CodeLens[], vscode.CodeLens[] | undefined>('A promise that resolves to an array of VoidCodeLens-instances.', (value, _args, converter) => {
// return tryMapWith<languages.CodeLens, vscode.CodeLens>(item => {
// return new types.CodeLens(typeConverters.Range.to(item.range), item.command && converter.fromInternal(item.command));
// })(value);
// })
// ),
// --- code actions // --- code actions
new ApiCommand( new ApiCommand(
'vscode.executeCodeActionProvider', '_executeCodeActionProvider', 'Execute code action provider.', 'vscode.executeCodeActionProvider', '_executeCodeActionProvider', 'Execute code action provider.',

View file

@ -2312,15 +2312,15 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
} }
$provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise<extHostProtocol.ICodeLensListDto | undefined> { $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise<extHostProtocol.ICodeLensListDto | undefined> {
return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), undefined, token); return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), undefined, token, resource.scheme === 'output');
} }
$resolveCodeLens(handle: number, symbol: extHostProtocol.ICodeLensDto, token: CancellationToken): Promise<extHostProtocol.ICodeLensDto | undefined> { $resolveCodeLens(handle: number, symbol: extHostProtocol.ICodeLensDto, token: CancellationToken): Promise<extHostProtocol.ICodeLensDto | undefined> {
return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined, undefined); return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined, undefined, true);
} }
$releaseCodeLenses(handle: number, cacheId: number): void { $releaseCodeLenses(handle: number, cacheId: number): void {
this._withAdapter(handle, CodeLensAdapter, adapter => Promise.resolve(adapter.releaseCodeLenses(cacheId)), undefined, undefined); this._withAdapter(handle, CodeLensAdapter, adapter => Promise.resolve(adapter.releaseCodeLenses(cacheId)), undefined, undefined, true);
} }
// --- declaration // --- declaration

View file

@ -584,7 +584,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
} }
const parsedInclude = include ? parseSearchExcludeInclude(GlobPattern.from(include)) : undefined; const parsedInclude = include ? parseSearchExcludeInclude(GlobPattern.from(include)) : undefined;
const excludePatterns = include ? globsToISearchPatternBuilder(options.exclude) : undefined; const excludePatterns = globsToISearchPatternBuilder(options.exclude);
return { return {
options: { options: {
@ -664,7 +664,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
async findTextInFilesBase(query: vscode.TextSearchQuery, queryOptions: QueryOptions<ITextQueryBuilderOptions>[] | undefined, callback: (result: ITextSearchResult<URI>, uri: URI) => void, token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.TextSearchComplete> { async findTextInFilesBase(query: vscode.TextSearchQuery, queryOptions: QueryOptions<ITextQueryBuilderOptions>[] | undefined, callback: (result: ITextSearchResult<URI>, uri: URI) => void, token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.TextSearchComplete> {
const requestId = this._requestIdProvider.getNext(); const requestId = this._requestIdProvider.getNext();
const isCanceled = false; let isCanceled = false;
token.onCancellationRequested(_ => {
isCanceled = true;
});
this._activeSearchCallbacks[requestId] = p => { this._activeSearchCallbacks[requestId] = p => {
if (isCanceled) { if (isCanceled) {

View file

@ -138,11 +138,11 @@
color: var(--vscode-titleBar-activeForeground); color: var(--vscode-titleBar-activeForeground);
} }
.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { .monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label {
color: var(--vscode-titleBar-inactiveForeground); color: var(--vscode-titleBar-inactiveForeground);
} }
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label {
color: inherit; color: inherit;
} }
@ -182,7 +182,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple { .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple {
justify-content: flex-start; justify-content: flex-start;
padding: 0 12px; padding: 0 12px;
} }
@ -280,7 +280,7 @@
border-left: 1px solid transparent; border-left: 1px solid transparent;
} }
/* Window Controls (Minimize, Max/Restore, Close) */ /* Window Controls Container */
.monaco-workbench .part.titlebar .window-controls-container { .monaco-workbench .part.titlebar .window-controls-container {
display: flex; display: flex;
flex-grow: 0; flex-grow: 0;
@ -292,7 +292,12 @@
height: 100%; height: 100%;
} }
/* Web WCO Sizing/Ordering */ .monaco-workbench.fullscreen .part.titlebar .window-controls-container {
display: none;
background-color: transparent;
}
/* Window Controls Container Web: Apply WCO environment variables (https://developer.mozilla.org/en-US/docs/Web/CSS/env#titlebar-area-x) */
.monaco-workbench.web .part.titlebar .titlebar-right .window-controls-container { .monaco-workbench.web .part.titlebar .titlebar-right .window-controls-container {
width: calc(100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px)); width: calc(100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px));
height: env(titlebar-area-height, 35px); height: env(titlebar-area-height, 35px);
@ -311,29 +316,31 @@
order: 1; order: 1;
} }
/* Desktop Windows/Linux Window Controls*/ /* Window Controls Container Desktop: apply zoom friendly size */
.monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container.primary { .monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container {
width: calc(138px / var(--zoom-factor, 1)); width: calc(138px / var(--zoom-factor, 1));
} }
.monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container.counter-zoom .window-controls-container.primary { .monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container.counter-zoom .window-controls-container {
width: 138px; width: 138px;
} }
.monaco-workbench.linux:not(.web) .part.titlebar .window-controls-container.wco-enabled {
width: calc(var(--title-wco-width, 138px));
}
.monaco-workbench.linux:not(.web) .part.titlebar .titlebar-container.counter-zoom .window-controls-container.wco-enabled {
width: var(--title-wco-width, 138px);
}
.monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container:not(.counter-zoom) .window-controls-container * { .monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container:not(.counter-zoom) .window-controls-container * {
zoom: calc(1 / var(--zoom-factor, 1)); zoom: calc(1 / var(--zoom-factor, 1));
} }
/* Desktop macOS Window Controls */ .monaco-workbench:not(.web).mac .part.titlebar .window-controls-container {
.monaco-workbench:not(.web).mac .part.titlebar .window-controls-container.primary {
width: 70px; width: 70px;
} }
.monaco-workbench.fullscreen .part.titlebar .window-controls-container {
display: none;
background-color: transparent;
}
/* Window Control Icons */ /* Window Control Icons */
.monaco-workbench .part.titlebar .window-controls-container > .window-icon { .monaco-workbench .part.titlebar .window-controls-container > .window-icon {
display: flex; display: flex;
@ -342,6 +349,11 @@
height: 100%; height: 100%;
width: 46px; width: 46px;
font-size: 16px; font-size: 16px;
color: var(--vscode-titleBar-activeForeground);
}
.monaco-workbench .part.titlebar.inactive .window-controls-container > .window-icon {
color: var(--vscode-titleBar-inactiveForeground);
} }
.monaco-workbench .part.titlebar .window-controls-container > .window-icon::before { .monaco-workbench .part.titlebar .window-controls-container > .window-icon::before {
@ -376,7 +388,6 @@
z-index: 2500; z-index: 2500;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
height: 100%; height: 100%;
min-width: 28px;
} }
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-right > .action-toolbar-container { .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-right > .action-toolbar-container {
@ -455,11 +466,3 @@
border-radius: 16px; border-radius: 16px;
text-align: center; text-align: center;
} }
.monaco-workbench .part.titlebar .window-controls-container .window-icon {
color: var(--vscode-titleBar-activeForeground);
}
.monaco-workbench .part.titlebar.inactive .window-controls-container .window-icon {
color: var(--vscode-titleBar-inactiveForeground);
}

View file

@ -7,7 +7,7 @@ import 'vs/css!./media/titlebarpart';
import { localize, localize2 } from 'vs/nls'; import { localize, localize2 } from 'vs/nls';
import { MultiWindowParts, Part } from 'vs/workbench/browser/part'; import { MultiWindowParts, Part } from 'vs/workbench/browser/part';
import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService';
import { getWCOBoundingRect, getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; import { getWCOTitlebarAreaRect, getZoomFactor, isWCOEnabled, onDidChangeZoomLevel } from 'vs/base/browser/browser';
import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@ -228,7 +228,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
const wcoEnabled = isWeb && isWCOEnabled(); const wcoEnabled = isWeb && isWCOEnabled();
let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30; let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30;
if (wcoEnabled) { if (wcoEnabled) {
value = Math.max(value, getWCOBoundingRect()?.height ?? 0); value = Math.max(value, getWCOTitlebarAreaRect(getWindow(this.element))?.height ?? 0);
} }
return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1); return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);
@ -249,7 +249,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
//#endregion //#endregion
protected rootContainer!: HTMLElement; protected rootContainer!: HTMLElement;
protected primaryWindowControls: HTMLElement | undefined; protected windowControlsContainer: HTMLElement | undefined;
protected dragRegion: HTMLElement | undefined; protected dragRegion: HTMLElement | undefined;
private title!: HTMLElement; private title!: HTMLElement;
@ -476,21 +476,49 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
this.createActionToolBarMenus(); this.createActionToolBarMenus();
} }
let primaryControlLocation = isMacintosh ? 'left' : 'right'; // Window Controls Container
if (isMacintosh && isNative) {
// Check if the locale is RTL, macOS will move traffic lights in RTL locales
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo
const localeInfo = new Intl.Locale(platformLocale) as any;
if (localeInfo?.textInfo?.direction === 'rtl') {
primaryControlLocation = 'right';
}
}
if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) { if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {
this.primaryWindowControls = append(primaryControlLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container.primary')); let primaryWindowControlsLocation = isMacintosh ? 'left' : 'right';
append(primaryControlLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container.secondary')); if (isMacintosh && isNative) {
// Check if the locale is RTL, macOS will move traffic lights in RTL locales
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo
const localeInfo = new Intl.Locale(platformLocale) as any;
if (localeInfo?.textInfo?.direction === 'rtl') {
primaryWindowControlsLocation = 'right';
}
}
if (isMacintosh && isNative && primaryWindowControlsLocation === 'left') {
// macOS native: controls are on the left and the container is not needed to make room
// for something, except for web where a custom menu being supported). not putting the
// container helps with allowing to move the window when clicking very close to the
// window control buttons.
} else {
this.windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));
if (isWeb) {
// Web: its possible to have control overlays on both sides, for example on macOS
// with window controls on the left and PWA controls on the right.
append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container'));
}
if (isWCOEnabled()) {
this.windowControlsContainer.classList.add('wco-enabled');
const updateWCOWidthVariable = () => {
const targetWindow = getWindow(this.element);
const wcoTitlebarAreaRect = getWCOTitlebarAreaRect(targetWindow);
if (wcoTitlebarAreaRect) {
const wcoWidth = targetWindow.innerWidth - wcoTitlebarAreaRect.width - wcoTitlebarAreaRect.x;
this.windowControlsContainer?.style.setProperty('--title-wco-width', `${wcoWidth}px`);
}
};
updateWCOWidthVariable();
this._register(onDidChangeZoomLevel(() => setTimeout(() => updateWCOWidthVariable(), 5))); // Somehow it does not get the right size without this timeout :-/
}
}
} }
// Context menu over title bar: depending on the OS and the location of the click this will either be // Context menu over title bar: depending on the OS and the location of the click this will either be

View file

@ -152,7 +152,7 @@ class ChatHistoryAction extends Action2 {
let lastDate: string | undefined = undefined; let lastDate: string | undefined = undefined;
const picks = items.flatMap((i): [IQuickPickSeparator | undefined, IChatPickerItem] => { const picks = items.flatMap((i): [IQuickPickSeparator | undefined, IChatPickerItem] => {
const timeAgoStr = fromNowByDay(i.lastMessageDate, true); const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true);
const separator: IQuickPickSeparator | undefined = timeAgoStr !== lastDate ? { const separator: IQuickPickSeparator | undefined = timeAgoStr !== lastDate ? {
type: 'separator', label: timeAgoStr, type: 'separator', label: timeAgoStr,
} : undefined; } : undefined;

View file

@ -36,7 +36,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions';
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView';
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
@ -261,9 +261,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlas
Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer);
registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup);
registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
// Disabled until https://github.com/microsoft/vscode/issues/218646 is fixed
// registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
registerChatActions(); registerChatActions();
registerChatCopyActions(); registerChatCopyActions();

View file

@ -516,7 +516,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
onDidChangeCursorPosition(); onDidChangeCursorPosition();
} }
private initAttachedContext(container: HTMLElement) { private initAttachedContext(container: HTMLElement, isLayout = false) {
const oldHeight = container.offsetHeight; const oldHeight = container.offsetHeight;
dom.clearNode(container); dom.clearNode(container);
this.attachedContextDisposables.clear(); this.attachedContextDisposables.clear();
@ -578,7 +578,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.attachedContextDisposables.add(disp); this.attachedContextDisposables.add(disp);
}); });
if (oldHeight !== container.offsetHeight) { if (oldHeight !== container.offsetHeight && !isLayout) {
this._onDidChangeHeight.fire(); this._onDidChangeHeight.fire();
} }
} }
@ -609,7 +609,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private previousInputEditorDimension: IDimension | undefined; private previousInputEditorDimension: IDimension | undefined;
private _layout(height: number, width: number, allowRecurse = true): void { private _layout(height: number, width: number, allowRecurse = true): void {
this.initAttachedContext(this.attachedContextContainer); this.initAttachedContext(this.attachedContextContainer, true);
const data = this.getLayoutData(); const data = this.getLayoutData();

View file

@ -3,17 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays';
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings'; import * as strings from 'vs/base/common/strings';
import { localize, localize2 } from 'vs/nls'; import { localize, localize2 } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Severity } from 'vs/platform/notification/common/notification';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
@ -21,7 +20,9 @@ import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer
import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat'; import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat';
import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_CHAT_EXTENSION_INVALID, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
@ -160,35 +161,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
}, },
}); });
export class ChatCompatibilityNotifier implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatCompatNotifier';
constructor(
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@INotificationService notificationService: INotificationService,
@ICommandService commandService: ICommandService
) {
// It may be better to have some generic UI for this, for any extension that is incompatible,
// but this is only enabled for Copilot Chat now and it needs to be obvious.
extensionsWorkbenchService.queryLocal().then(exts => {
const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat');
if (chat?.local?.validations.some(v => v[0] === Severity.Error)) {
notificationService.notify({
severity: Severity.Error,
message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."),
actions: {
primary: [
new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => {
return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']);
})
]
}
});
}
});
}
}
export class ChatExtensionPointHandler implements IWorkbenchContribution { export class ChatExtensionPointHandler implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; static readonly ID = 'workbench.contrib.chatExtensionPointHandler';
@ -198,9 +170,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
constructor( constructor(
@IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatAgentService private readonly _chatAgentService: IChatAgentService,
@ILogService private readonly logService: ILogService, @ILogService private readonly logService: ILogService
) { ) {
this._viewContainer = this.registerViewContainer(); this._viewContainer = this.registerViewContainer();
this.registerDefaultParticipantView();
this.handleAndRegisterChatExtensions(); this.handleAndRegisterChatExtensions();
} }
@ -239,11 +212,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
continue; continue;
} }
const store = new DisposableStore();
if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) {
store.add(this.registerDefaultParticipantView(providerDescriptor));
}
const participantsAndCommandsDisambiguation: { const participantsAndCommandsDisambiguation: {
categoryName: string; categoryName: string;
description: string; description: string;
@ -260,6 +228,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
} }
} }
const store = new DisposableStore();
store.add(this._chatAgentService.registerAgent( store.add(this._chatAgentService.registerAgent(
providerDescriptor.id, providerDescriptor.id,
{ {
@ -318,15 +287,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
return viewContainer; return viewContainer;
} }
private hasRegisteredDefaultParticipantView = false; private registerDefaultParticipantView(): IDisposable {
private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { // Register View. Name must be hardcoded because we want to show it even when the extension fails to load due to an API version incompatibility.
if (this.hasRegisteredDefaultParticipantView) { const name = 'GitHub Copilot';
this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`);
return Disposable.None;
}
// Register View
const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name;
const viewDescriptor: IViewDescriptor[] = [{ const viewDescriptor: IViewDescriptor[] = [{
id: CHAT_VIEW_ID, id: CHAT_VIEW_ID,
containerIcon: this._viewContainer.icon, containerIcon: this._viewContainer.icon,
@ -336,12 +299,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
canToggleVisibility: false, canToggleVisibility: false,
canMoveView: true, canMoveView: true,
ctorDescriptor: new SyncDescriptor(ChatViewPane), ctorDescriptor: new SyncDescriptor(ChatViewPane),
when: ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_EXTENSION_INVALID)
}]; }];
this.hasRegisteredDefaultParticipantView = true;
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer);
return toDisposable(() => { return toDisposable(() => {
this.hasRegisteredDefaultParticipantView = false;
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer);
}); });
} }
@ -350,3 +312,39 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
return `${extensionId.value}_${participantName}`; return `${extensionId.value}_${participantName}`;
} }
export class ChatCompatibilityNotifier implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatCompatNotifier';
constructor(
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IContextKeyService contextKeyService: IContextKeyService,
@IChatAgentService chatAgentService: IChatAgentService,
) {
// It may be better to have some generic UI for this, for any extension that is incompatible,
// but this is only enabled for Copilot Chat now and it needs to be obvious.
const showExtensionLabel = localize('showExtension', "Show Extension");
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
viewsRegistry.registerViewWelcomeContent(CHAT_VIEW_ID, {
content: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date.") + `\n\n[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([['GitHub.copilot-chat']]))})`,
when: CONTEXT_CHAT_EXTENSION_INVALID,
});
const isInvalid = CONTEXT_CHAT_EXTENSION_INVALID.bindTo(contextKeyService);
extensionsWorkbenchService.queryLocal().then(exts => {
const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat');
if (chat?.local?.validations.some(v => v[0] === Severity.Error)) {
// This catches vscode starting up with the invalid extension, but the extension may still get updated by vscode after this.
isInvalid.set(true);
}
});
const listener = chatAgentService.onDidChangeAgents(() => {
if (chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) {
isInvalid.set(false);
listener.dispose();
}
});
}
}

View file

@ -88,8 +88,9 @@ export class ChatViewPane extends ViewPane {
} else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) {
// Model is initialized, and the default agent disappeared, so show welcome view // Model is initialized, and the default agent disappeared, so show welcome view
this.didUnregisterProvider = true; this.didUnregisterProvider = true;
this._onDidChangeViewWelcomeState.fire();
} }
this._onDidChangeViewWelcomeState.fire();
})); }));
} }
@ -114,6 +115,10 @@ export class ChatViewPane extends ViewPane {
} }
override shouldShowWelcome(): boolean { override shouldShowWelcome(): boolean {
if (!this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel)) {
return true;
}
const noPersistedSessions = !this.chatService.hasSessions(); const noPersistedSessions = !this.chatService.hasSessions();
return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail);
} }

View file

@ -770,15 +770,11 @@ export class CodeCompareBlockPart extends Disposable {
}); });
dom.reset(this.messageElement, message); dom.reset(this.messageElement, message);
} }
const diffData = await data.diffData; const diffData = await data.diffData;
if (!diffData) {
return;
}
if (!isEditApplied) { if (!isEditApplied && diffData) {
const viewModel = this.diffEditor.createViewModel({ const viewModel = this.diffEditor.createViewModel({
original: diffData.original, original: diffData.original,
modified: diffData.modified modified: diffData.modified
@ -801,6 +797,7 @@ export class CodeCompareBlockPart extends Disposable {
} else { } else {
this.diffEditor.setModel(null); this.diffEditor.setModel(null);
this._lastDiffEditorViewModel.value = undefined; this._lastDiffEditorViewModel.value = undefined;
this._onDidChangeContentHeight.fire();
} }
this.toolbar.context = { this.toolbar.context = {

View file

@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService'; import { IProductService } from 'vs/platform/product/common/productService';
import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { asJson, IRequestService } from 'vs/platform/request/common/request';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService';
@ -233,11 +233,13 @@ export class ChatAgentService implements IChatAgentService {
readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event; readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event;
private readonly _hasDefaultAgent: IContextKey<boolean>; private readonly _hasDefaultAgent: IContextKey<boolean>;
private readonly _defaultAgentRegistered: IContextKey<boolean>;
constructor( constructor(
@IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextKeyService private readonly contextKeyService: IContextKeyService,
) { ) {
this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService);
this._defaultAgentRegistered = CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.bindTo(this.contextKeyService);
} }
registerAgent(id: string, data: IChatAgentData): IDisposable { registerAgent(id: string, data: IChatAgentData): IDisposable {
@ -246,6 +248,10 @@ export class ChatAgentService implements IChatAgentService {
throw new Error(`Agent already registered: ${JSON.stringify(id)}`); throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
} }
if (data.isDefault) {
this._defaultAgentRegistered.set(true);
}
const that = this; const that = this;
const commands = data.slashCommands; const commands = data.slashCommands;
data = { data = {
@ -256,8 +262,13 @@ export class ChatAgentService implements IChatAgentService {
}; };
const entry = { data }; const entry = { data };
this._agents.set(id, entry); this._agents.set(id, entry);
this._onDidChangeAgents.fire(undefined);
return toDisposable(() => { return toDisposable(() => {
this._agents.delete(id); this._agents.delete(id);
if (data.isDefault) {
this._defaultAgentRegistered.set(false);
}
this._onDidChangeAgents.fire(undefined); this._onDidChangeAgents.fire(undefined);
}); });
} }
@ -445,7 +456,7 @@ export class ChatAgentService implements IChatAgentService {
const participants = this.getAgents().reduce<IChatParticipantMetadata[]>((acc, a) => { const participants = this.getAgents().reduce<IChatParticipantMetadata[]>((acc, a) => {
acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] }); acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] });
for (const command of a.slashCommands) { for (const command of a.slashCommands) {
acc.push({ participant: a.id, command: command.name, disambiguation: [] }); acc.push({ participant: a.id, command: command.name, disambiguation: command.disambiguation ?? [] });
} }
return acc; return acc;
}, []); }, []);

View file

@ -25,7 +25,9 @@ export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey<boolean>('chatInpu
export const CONTEXT_IN_CHAT_INPUT = new RawContextKey<boolean>('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_INPUT = new RawContextKey<boolean>('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") });
export const CONTEXT_IN_CHAT_SESSION = new RawContextKey<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") });
export const CONTEXT_CHAT_ENABLED = new RawContextKey<boolean>('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is registered.") }); export const CONTEXT_CHAT_ENABLED = new RawContextKey<boolean>('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") });
export const CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED = new RawContextKey<boolean>('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") });
export const CONTEXT_CHAT_EXTENSION_INVALID = new RawContextKey<boolean>('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") });
export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey<boolean>('chatCursorAtTop', false); export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey<boolean>('chatCursorAtTop', false);
export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey<boolean>('chatInputHasAgent', false); export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey<boolean>('chatInputHasAgent', false);
export const CONTEXT_CHAT_LOCATION = new RawContextKey<ChatAgentLocation>('chatLocation', undefined); export const CONTEXT_CHAT_LOCATION = new RawContextKey<ChatAgentLocation>('chatLocation', undefined);

View file

@ -616,6 +616,8 @@ export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChat
* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too. * TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.
*/ */
export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData { export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {
normalizeOldFields(raw);
if (!('version' in raw)) { if (!('version' in raw)) {
return { return {
version: 3, version: 3,
@ -636,6 +638,30 @@ export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISe
return raw; return raw;
} }
function normalizeOldFields(raw: ISerializableChatDataIn): void {
// Fill in fields that very old chat data may be missing
if (!raw.sessionId) {
raw.sessionId = generateUuid();
}
if (!raw.creationDate) {
raw.creationDate = getLastYearDate();
}
if ('version' in raw && (raw.version === 2 || raw.version === 3)) {
if (!raw.lastMessageDate) {
// A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing.
raw.lastMessageDate = getLastYearDate();
}
}
}
function getLastYearDate(): number {
const lastYearDate = new Date();
lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);
return lastYearDate.getTime();
}
export function isExportableSessionData(obj: unknown): obj is IExportableChatData { export function isExportableSessionData(obj: unknown): obj is IExportableChatData {
const data = obj as IExportableChatData; const data = obj as IExportableChatData;
return typeof data === 'object' && return typeof data === 'object' &&

View file

@ -481,9 +481,8 @@ export class ChatService extends Disposable implements IChatService {
} }
async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise<IChatSendRequestData | undefined> { async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise<IChatSendRequestData | undefined> {
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
if (!request.trim()) { if (!request.trim() && !options?.slashCommand && !options?.agentId) {
this.trace('sendRequest', 'Rejected empty message'); this.trace('sendRequest', 'Rejected empty message');
return; return;
} }
@ -546,6 +545,7 @@ export class ChatService extends Disposable implements IChatService {
const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);
const requests = [...model.getRequests()];
let gotProgress = false; let gotProgress = false;
const requestType = commandPart ? 'slashCommand' : 'string'; const requestType = commandPart ? 'slashCommand' : 'string';
@ -639,7 +639,7 @@ export class ChatService extends Disposable implements IChatService {
if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && enableCommandDetection) { if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && enableCommandDetection) {
// We have no agent or command to scope history with, pass the full history to the participant detection provider // We have no agent or command to scope history with, pass the full history to the participant detection provider
const defaultAgentHistory = this.getHistoryEntriesFromModel(model, location, defaultAgent.id); const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id);
// Prepare the request object that we will send to the participant detection provider // Prepare the request object that we will send to the participant detection provider
const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command, enableCommandDetection, undefined, false); const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command, enableCommandDetection, undefined, false);
@ -658,7 +658,7 @@ export class ChatService extends Disposable implements IChatService {
await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`);
// Recompute history in case the agent or command changed // Recompute history in case the agent or command changed
const history = this.getHistoryEntriesFromModel(model, location, agent.id); const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id);
const requestProps = await prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); const requestProps = await prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent);
const pendingRequest = this._pendingRequests.get(sessionId); const pendingRequest = this._pendingRequests.get(sessionId);
if (pendingRequest && !pendingRequest.requestId) { if (pendingRequest && !pendingRequest.requestId) {
@ -668,7 +668,7 @@ export class ChatService extends Disposable implements IChatService {
const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);
rawResult = agentResult; rawResult = agentResult;
agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);
chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model, location, agent.id), CancellationToken.None) : undefined; chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined;
} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {
request = model.addRequest(parsedRequest, { variables: [] }, attempt); request = model.addRequest(parsedRequest, { variables: [] }, attempt);
completeResponseCreated(); completeResponseCreated();
@ -775,9 +775,9 @@ export class ChatService extends Disposable implements IChatService {
}; };
} }
private getHistoryEntriesFromModel(model: IChatModel, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] {
const history: IChatAgentHistoryEntry[] = []; const history: IChatAgentHistoryEntry[] = [];
for (const request of model.getRequests()) { for (const request of requests) {
if (!request.response) { if (!request.response) {
continue; continue;
} }
@ -791,7 +791,7 @@ export class ChatService extends Disposable implements IChatService {
const promptTextResult = getPromptText(request.message); const promptTextResult = getPromptText(request.message);
const historyRequest: IChatAgentRequest = { const historyRequest: IChatAgentRequest = {
sessionId: model.sessionId, sessionId: sessionId,
requestId: request.id, requestId: request.id,
agentId: request.response.agent?.id ?? '', agentId: request.response.agent?.id ?? '',
message: promptTextResult.message, message: promptTextResult.message,

View file

@ -941,12 +941,6 @@ export class StopReadAloud extends Action2 {
when: ScopedChatSynthesisInProgress, when: ScopedChatSynthesisInProgress,
group: 'navigation', group: 'navigation',
order: -1 order: -1
},
{
id: MENU_INLINE_CHAT_WIDGET_SECONDARY,
when: ScopedChatSynthesisInProgress,
group: 'navigation',
order: -1
} }
] ]
}); });
@ -980,6 +974,15 @@ export class StopReadChatItemAloud extends Action2 {
CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered
), ),
group: 'navigation' group: 'navigation'
},
{
id: MENU_INLINE_CHAT_WIDGET_SECONDARY,
when: ContextKeyExpr.and(
ScopedChatSynthesisInProgress, // only when in progress
CONTEXT_RESPONSE, // only for responses
CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered
),
group: 'navigation'
} }
] ]
}); });

View file

@ -17,7 +17,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe
import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageService } from 'vs/platform/storage/common/storage';
import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, ISerializableChatData1, ISerializableChatData2, normalizeSerializableChatData, Response } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@ -230,4 +230,53 @@ suite('normalizeSerializableChatData', () => {
assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate); assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate);
assert.strictEqual(newData.customTitle, v2Data.computedTitle); assert.strictEqual(newData.customTitle, v2Data.computedTitle);
}); });
test('old bad data', () => {
const v1Data: ISerializableChatData1 = {
// Testing the scenario where these are missing
sessionId: undefined!,
creationDate: undefined!,
initialLocation: undefined,
isImported: false,
requesterAvatarIconUri: undefined,
requesterUsername: 'me',
requests: [],
responderAvatarIconUri: undefined,
responderUsername: 'bot',
welcomeMessage: []
};
const newData = normalizeSerializableChatData(v1Data);
assert.strictEqual(newData.version, 3);
assert.ok(newData.creationDate > 0);
assert.ok(newData.lastMessageDate > 0);
assert.ok(newData.sessionId);
});
test('v3 with bug', () => {
const v3Data: ISerializableChatData3 = {
// Test case where old data was wrongly normalized and these fields were missing
creationDate: undefined!,
lastMessageDate: undefined!,
version: 3,
initialLocation: undefined,
isImported: false,
requesterAvatarIconUri: undefined,
requesterUsername: 'me',
requests: [],
responderAvatarIconUri: undefined,
responderUsername: 'bot',
sessionId: 'session1',
welcomeMessage: [],
customTitle: 'computed title'
};
const newData = normalizeSerializableChatData(v3Data);
assert.strictEqual(newData.version, 3);
assert.ok(newData.creationDate > 0);
assert.ok(newData.lastMessageDate > 0);
assert.ok(newData.sessionId);
});
}); });

View file

@ -172,7 +172,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
this.filters = this._register(new CommentsFilters({ this.filters = this._register(new CommentsFilters({
showResolved: this.viewState['showResolved'] !== false, showResolved: this.viewState['showResolved'] !== false,
showUnresolved: this.viewState['showUnresolved'] !== false, showUnresolved: this.viewState['showUnresolved'] !== false,
sortBy: this.viewState['sortBy'], sortBy: this.viewState['sortBy'] ?? CommentsSortOrder.ResourceAscending,
}, this.contextKeyService)); }, this.contextKeyService));
this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved));

View file

@ -7,7 +7,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
@ -74,9 +74,9 @@ export class CommentsFilters extends Disposable {
} }
} }
private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); private _sortBy: IContextKey<CommentsSortOrder> = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService);
get sortBy(): CommentsSortOrder { get sortBy(): CommentsSortOrder {
return this._sortBy.get()!; return this._sortBy.get() ?? CommentsSortOrder.ResourceAscending;
} }
set sortBy(sortBy: CommentsSortOrder) { set sortBy(sortBy: CommentsSortOrder) {
if (this._sortBy.get() !== sortBy) { if (this._sortBy.get() !== sortBy) {
@ -208,7 +208,7 @@ registerAction2(class extends ViewAction<ICommentsView> {
icon: Codicon.history, icon: Codicon.history,
viewId: COMMENTS_VIEW_ID, viewId: COMMENTS_VIEW_ID,
toggled: { toggled: {
condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending), condition: ContextKeyExpr.equals(CONTEXT_KEY_SORT_BY.key, CommentsSortOrder.UpdatedAtDescending),
title: localize('sorting by updated at', "Updated Time"), title: localize('sorting by updated at', "Updated Time"),
}, },
menu: { menu: {
@ -229,13 +229,13 @@ registerAction2(class extends ViewAction<ICommentsView> {
constructor() { constructor() {
super({ super({
id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByResource`, id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByResource`,
title: localize('toggle sorting by resource', "File"), title: localize('toggle sorting by resource', "Position in File"),
category: localize('comments', "Comments"), category: localize('comments', "Comments"),
icon: Codicon.history, icon: Codicon.history,
viewId: COMMENTS_VIEW_ID, viewId: COMMENTS_VIEW_ID,
toggled: { toggled: {
condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.ResourceAscending), condition: ContextKeyExpr.equals(CONTEXT_KEY_SORT_BY.key, CommentsSortOrder.ResourceAscending),
title: localize('sorting by file', "File"), title: localize('sorting by position in file', "Position in File"),
}, },
menu: { menu: {
id: commentSortSubmenu, id: commentSortSubmenu,

View file

@ -12,20 +12,24 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue } from 'vs/base/common/observable'; import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable';
import { ThemeIcon } from 'vs/base/common/themables'; import { ThemeIcon } from 'vs/base/common/themables';
import { Constants } from 'vs/base/common/uint'; import { Constants } from 'vs/base/common/uint';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import 'vs/css!./media/callStackWidget'; import 'vs/css!./media/callStackWidget';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorContributionCtor, EditorContributionInstantiation, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { Location } from 'vs/editor/common/languages'; import { Location } from 'vs/editor/common/languages';
import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';
import { localize, localize2 } from 'vs/nls'; import { localize, localize2 } from 'vs/nls';
import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
@ -38,7 +42,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { ResourceLabel } from 'vs/workbench/browser/labels'; import { ResourceLabel } from 'vs/workbench/browser/labels';
import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
export class CallStackFrame { export class CallStackFrame {
@ -97,6 +101,9 @@ class WrappedCustomStackFrame implements IFrameLikeItem {
constructor(public readonly original: CustomStackFrame) { } constructor(public readonly original: CustomStackFrame) { }
} }
const isFrameLike = (item: unknown): item is IFrameLikeItem =>
item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame;
type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame; type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame;
const WIDGET_CLASS_NAME = 'multiCallStackWidget'; const WIDGET_CLASS_NAME = 'multiCallStackWidget';
@ -137,6 +144,7 @@ export class CallStackWidget extends Disposable {
multipleSelectionSupport: false, multipleSelectionSupport: false,
mouseSupport: false, mouseSupport: false,
keyboardSupport: false, keyboardSupport: false,
setRowLineHeight: false,
accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider), accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider),
} }
) as WorkbenchList<ListItem>); ) as WorkbenchList<ListItem>);
@ -157,6 +165,17 @@ export class CallStackWidget extends Disposable {
this.layoutEmitter.fire(); this.layoutEmitter.fire();
} }
public collapseAll() {
transaction(tx => {
for (let i = 0; i < this.list.length; i++) {
const frame = this.list.element(i);
if (isFrameLike(frame)) {
frame.collapsed.set(true, tx);
}
}
});
}
private async loadFrame(replacing: SkippedCallFrames): Promise<void> { private async loadFrame(replacing: SkippedCallFrames): Promise<void> {
if (!this.cts) { if (!this.cts) {
return; return;
@ -356,9 +375,9 @@ abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateDat
collapse.element.ariaExpanded = String(!collapsed); collapse.element.ariaExpanded = String(!collapsed);
elements.root.classList.toggle('collapsed', collapsed); elements.root.classList.toggle('collapsed', collapsed);
})); }));
elementStore.add(collapse.onDidClick(() => { const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined);
item.collapsed.set(!item.collapsed.get(), undefined); elementStore.add(collapse.onDidClick(toggleCollapse));
})); elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse));
} }
disposeElement(element: ListItem, index: number, templateData: T, height: number | undefined): void { disposeElement(element: ListItem, index: number, templateData: T, height: number | undefined): void {
@ -382,26 +401,33 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
private readonly containingEditor: ICodeEditor | undefined, private readonly containingEditor: ICodeEditor | undefined,
private readonly onLayout: Event<void>, private readonly onLayout: Event<void>,
@ITextModelService private readonly modelService: ITextModelService, @ITextModelService private readonly modelService: ITextModelService,
@ICodeEditorService private readonly editorService: ICodeEditorService,
@IInstantiationService instantiationService: IInstantiationService, @IInstantiationService instantiationService: IInstantiationService,
) { ) {
super(instantiationService); super(instantiationService);
} }
protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData { protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData {
// override default e.g. language contributions, only allow users to click
// on code in the call stack to go to its source location
const contributions: IEditorContributionDescription[] = [{
id: ClickToLocationContribution.ID,
instantiation: EditorContributionInstantiation.BeforeFirstInteraction,
ctor: ClickToLocationContribution as EditorContributionCtor,
}];
const editor = this.containingEditor const editor = this.containingEditor
? this.instantiationService.createInstance( ? this.instantiationService.createInstance(
EmbeddedCodeEditorWidget, EmbeddedCodeEditorWidget,
data.elements.editor, data.elements.editor,
editorOptions, editorOptions,
{ isSimpleWidget: true }, { isSimpleWidget: true, contributions },
this.containingEditor, this.containingEditor,
) )
: this.instantiationService.createInstance( : this.instantiationService.createInstance(
CodeEditorWidget, CodeEditorWidget,
data.elements.editor, data.elements.editor,
editorOptions, editorOptions,
{ isSimpleWidget: true }, { isSimpleWidget: true, contributions },
); );
data.templateStore.add(editor); data.templateStore.add(editor);
@ -423,20 +449,6 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
const uri = item.source!; const uri = item.source!;
template.label.element.setFile(uri); template.label.element.setFile(uri);
template.elements.title.role = 'link';
elementStore.add(dom.addDisposableListener(template.elements.title, 'click', e => {
this.editorService.openCodeEditor({
resource: uri,
options: {
selection: Range.fromPositions({
column: item.column ?? 1,
lineNumber: item.line ?? 1,
}),
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
},
}, this.containingEditor || null, e.ctrlKey || e.metaKey);
}));
const cts = new CancellationTokenSource(); const cts = new CancellationTokenSource();
elementStore.add(toDisposable(() => cts.dispose(true))); elementStore.add(toDisposable(() => cts.dispose(true)));
this.modelService.createModelReference(uri).then(reference => { this.modelService.createModelReference(uri).then(reference => {
@ -632,6 +644,73 @@ class SkippedRenderer implements IListRenderer<ListItem, ISkippedTemplateData> {
} }
} }
/** A simple contribution that makes all data in the editor clickable to go to the location */
class ClickToLocationContribution extends Disposable implements IEditorContribution {
public static readonly ID = 'clickToLocation';
private readonly linkDecorations: IEditorDecorationsCollection;
private current: { line: number; word: IWordAtPosition } | undefined;
constructor(
private readonly editor: ICodeEditor,
@IEditorService editorService: IEditorService,
) {
super();
this.linkDecorations = editor.createDecorationsCollection();
this._register(toDisposable(() => this.linkDecorations.clear()));
const clickLinkGesture = this._register(new ClickLinkGesture(editor));
this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
this.onMove(mouseEvent);
}));
this._register(clickLinkGesture.onExecute((e) => {
const model = this.editor.getModel();
if (!this.current || !model) {
return;
}
editorService.openEditor({
resource: model.uri,
options: {
selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)),
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
},
}, e.hasSideBySideModifier ? SIDE_GROUP : undefined);
}));
}
private onMove(mouseEvent: ClickLinkMouseEvent) {
if (!mouseEvent.hasTriggerModifier) {
return this.clear();
}
const position = mouseEvent.target.position;
const word = position && this.editor.getModel()?.getWordAtPosition(position);
if (!word) {
return this.clear();
}
const prev = this.current?.word;
if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) {
return;
}
this.current = { word, line: position.lineNumber };
this.linkDecorations.set([{
range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
options: {
description: 'call-stack-go-to-file-link',
inlineClassName: 'call-stack-go-to-file-link',
},
}]);
}
private clear() {
this.linkDecorations.clear();
this.current = undefined;
}
}
registerAction2(class extends Action2 { registerAction2(class extends Action2 {
constructor() { constructor() {
super({ super({

View file

@ -24,6 +24,10 @@
&[role="link"] { &[role="link"] {
cursor: pointer; cursor: pointer;
} }
.monaco-icon-label::before {
height: auto;
}
} }
&.collapsed { &.collapsed {
@ -39,6 +43,7 @@
.collapse-button { .collapse-button {
width: 16px; width: 16px;
min-height: 1px; /* show even if empty */ min-height: 1px; /* show even if empty */
line-height: 0;
a { a {
cursor: pointer; cursor: pointer;
@ -56,6 +61,11 @@
.multiCallStackWidget { .multiCallStackWidget {
.multiCallStackFrameContainer { .multiCallStackFrameContainer {
background: none !important; background: none !important;
line-height: inherit !important;
} }
} }
.monaco-editor .call-stack-go-to-file-link {
text-decoration: underline;
cursor: pointer;
color: var(--vscode-editorLink-activeForeground) !important;
}

View file

@ -79,7 +79,6 @@ class ExtensionsViewState extends Disposable implements IExtensionsViewState {
} }
} }
export interface ExtensionsListViewOptions { export interface ExtensionsListViewOptions {
server?: IExtensionManagementServer; server?: IExtensionManagementServer;
flexibleHeight?: boolean; flexibleHeight?: boolean;

View file

@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { localize, localize2 } from 'vs/nls'; import { localize, localize2 } from 'vs/nls';
import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { Action2, IAction2Options } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
@ -287,7 +287,7 @@ export class DiscardHunkAction extends AbstractInlineChatAction {
constructor() { constructor() {
super({ super({
id: 'inlineChat.discardHunkChange', id: ACTION_DISCARD_CHANGES,
title: localize('discard', 'Discard'), title: localize('discard', 'Discard'),
icon: Codicon.chromeClose, icon: Codicon.chromeClose,
precondition: CTX_INLINE_CHAT_VISIBLE, precondition: CTX_INLINE_CHAT_VISIBLE,

View file

@ -39,7 +39,7 @@ import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget';
import { HunkInformation, Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { HunkInformation, HunkState, Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl';
import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
@ -622,6 +622,7 @@ export class InlineChatController implements IEditorContribution {
return; return;
} }
if (e.kind === 'move') { if (e.kind === 'move') {
assertType(this._session);
const log: typeof this._log = (msg: string, ...args: any[]) => this._log('state=_showRequest) moving inline chat', msg, ...args); const log: typeof this._log = (msg: string, ...args: any[]) => this._log('state=_showRequest) moving inline chat', msg, ...args);
log('move was requested', e.target, e.range); log('move was requested', e.target, e.range);
@ -636,13 +637,13 @@ export class InlineChatController implements IEditorContribution {
} }
const newEditor = editorPane.getControl(); const newEditor = editorPane.getControl();
if (!newEditor || !isCodeEditor(newEditor) || !newEditor.hasModel()) { if (!isCodeEditor(newEditor) || !newEditor.hasModel()) {
log('new editor is either missing or not a code editor or does not have a model'); log('new editor is either missing or not a code editor or does not have a model');
return; return;
} }
if (!this._session) { if (this._inlineChatSessionService.getSession(newEditor, e.target)) {
log('controller does not have a session'); log('new editor ALREADY has a session');
return; return;
} }
@ -741,7 +742,7 @@ export class InlineChatController implements IEditorContribution {
await responsePromise.p; await responsePromise.p;
await progressiveEditsQueue.whenIdle(); await progressiveEditsQueue.whenIdle();
if (response.isCanceled) { if (response.result?.errorDetails) {
await this._session.undoChangesUntil(response.requestId); await this._session.undoChangesUntil(response.requestId);
} }
@ -758,7 +759,6 @@ export class InlineChatController implements IEditorContribution {
if (response.result?.errorDetails) { if (response.result?.errorDetails) {
// //
await this._session.undoChangesUntil(response.requestId);
} else if (response.response.value.length === 0) { } else if (response.response.value.length === 0) {
// empty -> show message // empty -> show message
@ -990,8 +990,21 @@ export class InlineChatController implements IEditorContribution {
// ---- controller API // ---- controller API
showSaveHint(): void { showSaveHint(): void {
const status = localize('savehint', "Accept or discard changes to continue saving"); if (!this._session) {
return;
}
const status = localize('savehint', "Accept or discard changes to continue saving.");
this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] });
if (this._ui.value.zone.position) {
this._editor.revealLineInCenterIfOutsideViewport(this._ui.value.zone.position.lineNumber);
} else {
const hunk = this._session.hunkData.getInfo().find(info => info.getState() === HunkState.Pending);
if (hunk) {
this._editor.revealLineInCenterIfOutsideViewport(hunk.getRangesN()[0].startLineNumber);
}
}
} }
acceptInput() { acceptInput() {

View file

@ -263,7 +263,7 @@ export class StashedSession {
// keep session for a little bit, only release when user continues to work (type, move cursor, etc.) // keep session for a little bit, only release when user continues to work (type, move cursor, etc.)
this._session = session; this._session = session;
this._ctxHasStashedSession.set(true); this._ctxHasStashedSession.set(true);
this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel))(() => { this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel, editor.onDidBlurEditorWidget))(() => {
this._session = undefined; this._session = undefined;
this._sessionService.releaseSession(session); this._sessionService.releaseSession(session);
this._ctxHasStashedSession.reset(); this._ctxHasStashedSession.reset();
@ -360,27 +360,30 @@ export class HunkData {
// mirror textModelN changes to textModel0 execept for those that // mirror textModelN changes to textModel0 execept for those that
// overlap with a hunk // overlap with a hunk
type HunkRangePair = { rangeN: Range; range0: Range }; type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void };
const hunkRanges: HunkRangePair[] = []; const hunkRanges: HunkRangePair[] = [];
const ranges0: Range[] = []; const ranges0: Range[] = [];
for (const { textModelNDecorations, textModel0Decorations, state } of this._data.values()) { for (const entry of this._data.values()) {
if (state === HunkState.Pending) { if (entry.state === HunkState.Pending) {
// pending means the hunk's changes aren't "sync'd" yet // pending means the hunk's changes aren't "sync'd" yet
for (let i = 1; i < textModelNDecorations.length; i++) { for (let i = 1; i < entry.textModelNDecorations.length; i++) {
const rangeN = this._textModelN.getDecorationRange(textModelNDecorations[i]); const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]);
const range0 = this._textModel0.getDecorationRange(textModel0Decorations[i]); const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
if (rangeN && range0) { if (rangeN && range0) {
hunkRanges.push({ rangeN, range0 }); hunkRanges.push({
rangeN, range0,
markAccepted: () => entry.state = HunkState.Accepted
});
} }
} }
} else if (state === HunkState.Accepted) { } else if (entry.state === HunkState.Accepted) {
// accepted means the hunk's changes are also in textModel0 // accepted means the hunk's changes are also in textModel0
for (let i = 1; i < textModel0Decorations.length; i++) { for (let i = 1; i < entry.textModel0Decorations.length; i++) {
const range = this._textModel0.getDecorationRange(textModel0Decorations[i]); const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
if (range) { if (range) {
ranges0.push(range); ranges0.push(range);
} }
@ -399,16 +402,20 @@ export class HunkData {
let pendingChangesLen = 0; let pendingChangesLen = 0;
for (const { rangeN, range0 } of hunkRanges) { for (const entry of hunkRanges) {
if (rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) {
// pending hunk _before_ this change. When projecting into textModel0 we need to // pending hunk _before_ this change. When projecting into textModel0 we need to
// subtract that. Because diffing is relaxed it might include changes that are not // subtract that. Because diffing is relaxed it might include changes that are not
// actual insertions/deletions. Therefore we need to take the length of the original // actual insertions/deletions. Therefore we need to take the length of the original
// range into account. // range into account.
pendingChangesLen += this._textModelN.getValueLengthInRange(rangeN); pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN);
pendingChangesLen -= this._textModel0.getValueLengthInRange(range0); pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0);
} else if (Range.areIntersectingOrTouching(rangeN, change.range)) { } else if (Range.areIntersectingOrTouching(entry.rangeN, change.range)) {
// an edit overlaps with a (pending) hunk. We take this as a signal
// to mark the hunk as accepted and to ignore the edit. The range of the hunk
// will be up-to-date because of decorations created for them
entry.markAccepted();
isOverlapping = true; isOverlapping = true;
break; break;
@ -447,24 +454,23 @@ export class HunkData {
diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced');
if (!diff || diff.changes.length === 0) { let mergedChanges: DetailedLineRangeMapping[] = [];
// return new HunkData([], session);
return;
}
// merge changes neighboring changes if (diff && diff.changes.length > 0) {
const mergedChanges = [diff.changes[0]]; // merge changes neighboring changes
for (let i = 1; i < diff.changes.length; i++) { mergedChanges = [diff.changes[0]];
const lastChange = mergedChanges[mergedChanges.length - 1]; for (let i = 1; i < diff.changes.length; i++) {
const thisChange = diff.changes[i]; const lastChange = mergedChanges[mergedChanges.length - 1];
if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { const thisChange = diff.changes[i];
mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) {
lastChange.original.join(thisChange.original), mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping(
lastChange.modified.join(thisChange.modified), lastChange.original.join(thisChange.original),
(lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) lastChange.modified.join(thisChange.modified),
); (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? [])
} else { );
mergedChanges.push(thisChange); } else {
mergedChanges.push(thisChange);
}
} }
} }

View file

@ -8,7 +8,7 @@ import { coalesceInPlace } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { themeColorFromId } from 'vs/base/common/themables'; import { themeColorFromId, ThemeIcon } from 'vs/base/common/themables';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines'; import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines';
@ -27,7 +27,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { HunkInformation, Session, HunkState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { HunkInformation, Session, HunkState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { assertType } from 'vs/base/common/types'; import { assertType } from 'vs/base/common/types';
import { IModelService } from 'vs/editor/common/services/model'; import { IModelService } from 'vs/editor/common/services/model';
import { performAsyncTextEdit, asProgressiveEdit } from './utils'; import { performAsyncTextEdit, asProgressiveEdit } from './utils';
@ -43,6 +43,9 @@ import { generateUuid } from 'vs/base/common/uuid';
import { MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar';
import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Iterable } from 'vs/base/common/iterator'; import { Iterable } from 'vs/base/common/iterator';
import { ConflictActionsFactory, IContentWidgetAction } from 'vs/workbench/contrib/mergeEditor/browser/view/conflictActions';
import { observableValue } from 'vs/base/common/observable';
import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
export interface IEditObserver { export interface IEditObserver {
start(): void; start(): void;
@ -216,8 +219,10 @@ type HunkDisplayData = {
decorationIds: string[]; decorationIds: string[];
viewZoneId: string | undefined; diffViewZoneId: string | undefined;
viewZone: IViewZone; diffViewZone: IViewZone;
lensActionsViewZoneIds?: string[];
distance: number; distance: number;
position: Position; position: Position;
@ -257,6 +262,7 @@ export class LiveStrategy extends EditModeStrategy {
private readonly _ctxCurrentChangeShowsDiff: IContextKey<boolean>; private readonly _ctxCurrentChangeShowsDiff: IContextKey<boolean>;
private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; private readonly _progressiveEditingDecorations: IEditorDecorationsCollection;
private readonly _lensActionsFactory: ConflictActionsFactory;
private _editCount: number = 0; private _editCount: number = 0;
constructor( constructor(
@ -268,6 +274,8 @@ export class LiveStrategy extends EditModeStrategy {
@IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IConfigurationService private readonly _configService: IConfigurationService, @IConfigurationService private readonly _configService: IConfigurationService,
@IMenuService private readonly _menuService: IMenuService,
@IContextKeyService private readonly _contextService: IContextKeyService,
@ITextFileService textFileService: ITextFileService, @ITextFileService textFileService: ITextFileService,
@IInstantiationService instaService: IInstantiationService @IInstantiationService instaService: IInstantiationService
) { ) {
@ -276,6 +284,7 @@ export class LiveStrategy extends EditModeStrategy {
this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService);
this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); this._progressiveEditingDecorations = this._editor.createDecorationsCollection();
this._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor));
} }
@ -487,45 +496,95 @@ export class LiveStrategy extends EditModeStrategy {
afterLineNumber: -1, afterLineNumber: -1,
heightInLines: result.heightInLines, heightInLines: result.heightInLines,
domNode, domNode,
ordinal: 50000 + 1 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42
}; };
const toggleDiff = () => { const toggleDiff = () => {
const scrollState = StableEditorScrollState.capture(this._editor); const scrollState = StableEditorScrollState.capture(this._editor);
changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => { changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => {
assertType(data); assertType(data);
if (!data.viewZoneId) { if (!data.diffViewZoneId) {
const [hunkRange] = hunkData.getRangesN(); const [hunkRange] = hunkData.getRangesN();
viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1; viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1;
data.viewZoneId = viewZoneAccessor.addZone(viewZoneData); data.diffViewZoneId = viewZoneAccessor.addZone(viewZoneData);
overlay?.updateExtraTop(result.heightInLines); overlay?.updateExtraTop(result.heightInLines);
} else { } else {
viewZoneAccessor.removeZone(data.viewZoneId!); viewZoneAccessor.removeZone(data.diffViewZoneId!);
overlay?.updateExtraTop(0); overlay?.updateExtraTop(0);
data.viewZoneId = undefined; data.diffViewZoneId = undefined;
} }
}); });
this._ctxCurrentChangeShowsDiff.set(typeof data?.viewZoneId === 'string'); this._ctxCurrentChangeShowsDiff.set(typeof data?.diffViewZoneId === 'string');
scrollState.restore(this._editor); scrollState.restore(this._editor);
}; };
const overlay = this._showOverlayToolbar const overlay = this._showOverlayToolbar && false
? this._instaService.createInstance(InlineChangeOverlay, this._editor, hunkData) ? this._instaService.createInstance(InlineChangeOverlay, this._editor, hunkData)
: undefined; : undefined;
let lensActions: DisposableStore | undefined;
const lensActionsViewZoneIds: string[] = [];
if (this._showOverlayToolbar && hunkData.getState() === HunkState.Pending) {
lensActions = new DisposableStore();
const menu = this._menuService.createMenu(MENU_INLINE_CHAT_ZONE, this._contextService);
const makeActions = () => {
const actions: IContentWidgetAction[] = [];
const tuples = menu.getActions();
for (const [, group] of tuples) {
for (const item of group) {
if (item instanceof MenuItemAction) {
let text = item.label;
if (item.id === ACTION_TOGGLE_DIFF) {
text = item.checked ? 'Hide Changes' : 'Show Changes';
} else if (ThemeIcon.isThemeIcon(item.item.icon)) {
text = `$(${item.item.icon.id}) ${text}`;
}
actions.push({
text,
tooltip: item.tooltip,
action: async () => item.run(),
});
}
}
}
return actions;
};
const obs = observableValue(this, makeActions());
lensActions.add(menu.onDidChange(() => obs.set(makeActions(), undefined)));
lensActions.add(menu);
lensActions.add(this._lensActionsFactory.createWidget(viewZoneAccessor,
hunkRanges[0].startLineNumber - 1,
obs,
lensActionsViewZoneIds
));
}
const remove = () => { const remove = () => {
changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => {
assertType(data); assertType(data);
for (const decorationId of data.decorationIds) { for (const decorationId of data.decorationIds) {
decorationsAccessor.removeDecoration(decorationId); decorationsAccessor.removeDecoration(decorationId);
} }
if (data.viewZoneId) { if (data.diffViewZoneId) {
viewZoneAccessor.removeZone(data.viewZoneId); viewZoneAccessor.removeZone(data.diffViewZoneId);
} }
data.decorationIds = []; data.decorationIds = [];
data.viewZoneId = undefined; data.diffViewZoneId = undefined;
data.lensActionsViewZoneIds?.forEach(viewZoneAccessor.removeZone);
data.lensActionsViewZoneIds = undefined;
}); });
lensActions?.dispose();
overlay?.dispose(); overlay?.dispose();
}; };
@ -548,8 +607,9 @@ export class LiveStrategy extends EditModeStrategy {
data = { data = {
hunk: hunkData, hunk: hunkData,
decorationIds, decorationIds,
viewZoneId: '', diffViewZoneId: '',
viewZone: viewZoneData, diffViewZone: viewZoneData,
lensActionsViewZoneIds,
distance: myDistance, distance: myDistance,
position: hunkRanges[0].getStartPosition().delta(-1), position: hunkRanges[0].getStartPosition().delta(-1),
acceptHunk, acceptHunk,

View file

@ -96,9 +96,9 @@ export class InlineChatWidget {
h('div.accessibleViewer@accessibleViewer'), h('div.accessibleViewer@accessibleViewer'),
h('div.status@status', [ h('div.status@status', [
h('div.label.info.hidden@infoLabel'), h('div.label.info.hidden@infoLabel'),
h('div.actions.button-style.hidden@toolbar1'), h('div.actions.hidden@toolbar1'),
h('div.label.status.hidden@statusLabel'), h('div.label.status.hidden@statusLabel'),
h('div.actions.button-style.hidden@toolbar2'), h('div.actions.secondary.hidden@toolbar2'),
]), ]),
] ]
); );
@ -385,7 +385,7 @@ export class InlineChatWidget {
} }
protected _getExtraHeight(): number { protected _getExtraHeight(): number {
return 4 /* padding */ + 2 /*border*/ + 4 /*shadow*/; return 2 /*border*/ + 4 /*shadow*/;
} }
get value(): string { get value(): string {

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { addDisposableListener, Dimension } from 'vs/base/browser/dom'; import { addDisposableListener, Dimension } from 'vs/base/browser/dom';
import * as aria from 'vs/base/browser/ui/aria/aria'; import * as aria from 'vs/base/browser/ui/aria/aria';
import { toDisposable } from 'vs/base/common/lifecycle'; import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { assertType } from 'vs/base/common/types'; import { assertType } from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';
@ -29,6 +29,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: EditorBasedInlineChatWidget; readonly widget: EditorBasedInlineChatWidget;
private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor));
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
private _dimension?: Dimension; private _dimension?: Dimension;
@ -165,6 +166,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
this.widget.focus(); this.widget.focus();
revealZone(); revealZone();
this._scrollUp.enable();
} }
override updatePositionAndHeight(position: Position): void { override updatePositionAndHeight(position: Position): void {
@ -186,14 +188,15 @@ export class InlineChatZoneWidget extends ZoneWidget {
return isResponseVM(candidate) && candidate.response.value.length > 0; return isResponseVM(candidate) && candidate.response.value.length > 0;
}); });
if (hasResponse && zoneTop < scrollTop) { if (hasResponse && zoneTop < scrollTop || this._scrollUp.didScrollUp) {
// don't reveal the zone if it is already out of view (unless we are still getting ready) // don't reveal the zone if it is already out of view (unless we are still getting ready)
return () => { // or if an outside scroll-up happened (e.g the user scrolled up to see the new content)
return this._scrollUp.runIgnored(() => {
scrollState.restore(this.editor); scrollState.restore(this.editor);
}; });
} }
return () => { return this._scrollUp.runIgnored(() => {
scrollState.restore(this.editor); scrollState.restore(this.editor);
const scrollTop = this.editor.getScrollTop(); const scrollTop = this.editor.getScrollTop();
@ -216,7 +219,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
} }
}; });
} }
protected override revealRange(range: Range, isLastLine: boolean): void { protected override revealRange(range: Range, isLastLine: boolean): void {
@ -229,6 +232,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
override hide(): void { override hide(): void {
const scrollState = StableEditorBottomScrollState.capture(this.editor); const scrollState = StableEditorBottomScrollState.capture(this.editor);
this._scrollUp.disable();
this._ctxCursorPosition.reset(); this._ctxCursorPosition.reset();
this.widget.reset(); this.widget.reset();
this.widget.chatWidget.setVisible(false); this.widget.chatWidget.setVisible(false);
@ -237,3 +241,54 @@ export class InlineChatZoneWidget extends ZoneWidget {
scrollState.restore(this.editor); scrollState.restore(this.editor);
} }
} }
class ScrollUpState {
private _lastScrollTop: number = this._editor.getScrollTop();
private _didScrollUp?: boolean;
private _ignoreEvents = false;
private readonly _listener = new MutableDisposable();
constructor(private readonly _editor: ICodeEditor) { }
dispose(): void {
this._listener.dispose();
}
enable(): void {
this._didScrollUp = undefined;
this._listener.value = this._editor.onDidScrollChange(e => {
if (!e.scrollTopChanged || this._ignoreEvents) {
return;
}
const currentScrollTop = e.scrollTop;
if (currentScrollTop > this._lastScrollTop) {
this._listener.clear();
this._didScrollUp = true;
}
this._lastScrollTop = currentScrollTop;
});
}
disable(): void {
this._listener.clear();
this._didScrollUp = undefined;
}
runIgnored(callback: () => void): () => void {
return () => {
this._ignoreEvents = true;
try {
return callback();
} finally {
this._ignoreEvents = false;
}
};
}
get didScrollUp(): boolean | undefined {
return this._didScrollUp;
}
}

View file

@ -20,7 +20,7 @@
} }
.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part {
padding: 4px 6px 0 6px; padding: 2px 6px 0 6px;
} }
.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar {
@ -32,6 +32,12 @@
border-radius: 2px; border-radius: 2px;
} }
.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups {
margin: 2px 0 0 4px;
}
.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list { .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list {
padding: 4px 0 0 0; padding: 4px 0 0 0;
} }
@ -70,7 +76,15 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 6px 6px 0 6px padding-left: 6px;
padding-right: 6px;
}
.monaco-workbench .inline-chat > .status {
.label,
.actions {
padding-top: 6px;
}
} }
.monaco-workbench .inline-chat .status .actions.hidden { .monaco-workbench .inline-chat .status .actions.hidden {
@ -92,7 +106,8 @@
.monaco-workbench .inline-chat .status .label.status { .monaco-workbench .inline-chat .status .label.status {
margin-left: auto; margin-left: auto;
padding: 0 6px; padding-right: 6px;
padding-left: 6px;
} }
.monaco-workbench .inline-chat .status .label.hidden, .monaco-workbench .inline-chat .status .label.hidden,
@ -156,6 +171,16 @@
gap: 4px; gap: 4px;
} }
.monaco-workbench .inline-chat .status .actions.secondary {
display: none;
}
.monaco-workbench .inline-chat .status:hover .actions.secondary,
.monaco-workbench .inline-chat:focus .status .actions.secondary,
.monaco-workbench .inline-chat .status:focus-within .actions.secondary {
display: inherit;
}
.monaco-workbench .inline-chat-diff-overlay { .monaco-workbench .inline-chat-diff-overlay {
.monaco-button { .monaco-button {

View file

@ -116,6 +116,7 @@ export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey<InlineChatRespons
// --- (selected) action identifier // --- (selected) action identifier
export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges';
export const ACTION_DISCARD_CHANGES = 'inlineChat.discardHunkChange';
export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate';
export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat';
export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff';

View file

@ -779,7 +779,7 @@ suite('InteractiveChatController', function () {
}); });
test('Stopping/cancelling a request should undo its changes', async function () { test('Stopping/cancelling a request should NOT undo its changes', async function () {
model.setValue('World'); model.setValue('World');
@ -819,7 +819,7 @@ suite('InteractiveChatController', function () {
chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId); chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);
assert.strictEqual(await p2, undefined); assert.strictEqual(await p2, undefined);
assert.strictEqual(model.getValue(), 'World'); assert.strictEqual(model.getValue(), 'HelloWorld'); // CANCEL just stops the request and progressive typing but doesn't undo
}); });

View file

@ -60,6 +60,8 @@ import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/co
import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService'; import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService';
import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService';
import { MockLanguageModelToolsService } from 'vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService'; import { MockLanguageModelToolsService } from 'vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService';
import { IChatRequestModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { assertSnapshot } from 'vs/base/test/common/snapshot';
suite('InlineChatSession', function () { suite('InlineChatSession', function () {
@ -487,4 +489,90 @@ suite('InlineChatSession', function () {
inlineChatSessionService.releaseSession(session); inlineChatSessionService.releaseSession(session);
}); });
test('Pressing Escape after inline chat errored with "response filtered" leaves document dirty #7764', async function () {
const origValue = `class Foo {
private onError(error: string): void {
if (/The request timed out|The network connection was lost/i.test(error)) {
return;
}
error = error.replace(/See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information');
this.notificationService.notify({
severity: Severity.Error,
message: error,
source: nls.localize('update service', "Update Service"),
});
}
}`;
model.setValue(origValue);
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
const fakeRequest = new class extends mock<IChatRequestModel>() {
override get id() { return 'one'; }
};
session.markModelVersion(fakeRequest);
assert.strictEqual(editor.getModel().getLineCount(), 15);
await makeEditAsAi([EditOperation.replace(new Range(7, 1, 7, Number.MAX_SAFE_INTEGER), `error = error.replace(
/See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/,
'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information'
);`)]);
assert.strictEqual(editor.getModel().getLineCount(), 18);
// called when a response errors out
await session.undoChangesUntil(fakeRequest.id);
await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }, undefined);
assert.strictEqual(editor.getModel().getValue(), origValue);
session.hunkData.discardAll(); // called when dimissing the session
assert.strictEqual(editor.getModel().getValue(), origValue);
});
test('Apply Code\'s preview should be easier to undo/esc #7537', async function () {
model.setValue(`export function fib(n) {
if (n <= 0) return 0;
if (n === 1) return 0;
if (n === 2) return 1;
return fib(n - 1) + fib(n - 2);
}`);
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), `
let a = 0, b = 1, c;
for (let i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}`)]);
assert.strictEqual(session.hunkData.size, 1);
assert.strictEqual(session.hunkData.pending, 1);
assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Pending));
await assertSnapshot(editor.getModel().getValue(), { name: '1' });
await model.undo();
await assertSnapshot(editor.getModel().getValue(), { name: '2' });
// overlapping edits (even UNDO) mark edits as accepted
assert.strictEqual(session.hunkData.size, 1);
assert.strictEqual(session.hunkData.pending, 0);
assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Accepted));
// no further change when discarding
session.hunkData.discardAll(); // CANCEL
await assertSnapshot(editor.getModel().getValue(), { name: '2' });
});
}); });

View file

@ -54,8 +54,8 @@ export class ConflictActionsFactory extends Disposable {
newStyle += `${this._styleClassName} { font-family: var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}}`; newStyle += `${this._styleClassName} { font-family: var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}}`;
} }
this._styleElement.textContent = newStyle; this._styleElement.textContent = newStyle;
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); this._editor.getContainerDomNode().style?.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings); this._editor.getContainerDomNode().style?.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
} }
private _getLayoutInfo() { private _getLayoutInfo() {

View file

@ -33,6 +33,7 @@ export abstract class FixedZoneWidget extends Disposable {
domNode: document.createElement('div'), domNode: document.createElement('div'),
afterLineNumber: afterLineNumber, afterLineNumber: afterLineNumber,
heightInPx: height, heightInPx: height,
ordinal: 50000 + 1,
onComputedHeight: (height) => { onComputedHeight: (height) => {
this.widgetDomNode.style.height = `${height}px`; this.widgetDomNode.style.height = `${height}px`;
}, },

View file

@ -1074,7 +1074,7 @@ export class DeletedElement extends SingleSideDiffElement {
layout(state: IDiffElementLayoutState) { layout(state: IDiffElementLayoutState) {
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
if (state.editorHeight || state.outerWidth) { if ((state.editorHeight || state.outerWidth) && this._editor) {
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
this._editor.layout({ this._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
@ -1254,7 +1254,7 @@ export class InsertElement extends SingleSideDiffElement {
layout(state: IDiffElementLayoutState) { layout(state: IDiffElementLayoutState) {
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
if (state.editorHeight || state.outerWidth) { if ((state.editorHeight || state.outerWidth) && this._editor) {
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
this._editor.layout({ this._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
@ -1644,7 +1644,7 @@ export class ModifiedElement extends AbstractElementRenderer {
{ {
updateInfoRendering: () => renderSourceEditor(), updateInfoRendering: () => renderSourceEditor(),
checkIfModified: (cell) => { checkIfModified: (cell) => {
return cell.modified?.textModel.getValue() !== cell.original?.textModel.getValue() ? { reason: undefined } : false; return cell.modified?.textModel.getTextBufferHash() !== cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false;
}, },
getFoldingState: (cell) => cell.cellFoldingState, getFoldingState: (cell) => cell.cellFoldingState,
updateFoldingState: (cell, state) => cell.cellFoldingState = state, updateFoldingState: (cell, state) => cell.cellFoldingState = state,
@ -1660,7 +1660,7 @@ export class ModifiedElement extends AbstractElementRenderer {
const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer); const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer);
this._register(scopedContextKeyService); this._register(scopedContextKeyService);
const inputChanged = NOTEBOOK_DIFF_CELL_INPUT.bindTo(scopedContextKeyService); const inputChanged = NOTEBOOK_DIFF_CELL_INPUT.bindTo(scopedContextKeyService);
inputChanged.set(this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue()); inputChanged.set(this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash());
const ignoreWhitespace = NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE.bindTo(scopedContextKeyService); const ignoreWhitespace = NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE.bindTo(scopedContextKeyService);
const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
@ -1675,7 +1675,7 @@ export class ModifiedElement extends AbstractElementRenderer {
const refreshToolbar = () => { const refreshToolbar = () => {
const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
ignoreWhitespace.set(ignore); ignoreWhitespace.set(ignore);
const hasChanges = this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue(); const hasChanges = this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash();
inputChanged.set(hasChanges); inputChanged.set(hasChanges);
if (hasChanges) { if (hasChanges) {

View file

@ -222,7 +222,7 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB
layoutState: CellLayoutState.Uninitialized layoutState: CellLayoutState.Uninitialized
}; };
this.cellFoldingState = modified?.textModel?.getValue() !== original?.textModel?.getValue() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.cellFoldingState = modified?.getTextBufferHash() !== original?.getTextBufferHash() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed;
this.metadataFoldingState = PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed;
this.outputFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed;

View file

@ -441,16 +441,12 @@ function createDiffViewModels(instantiationService: IInstantiationService, confi
); );
} }
case 'unchanged': { case 'unchanged': {
const originalCell = originalModel.cells[diff.originalCellIndex];
const modifiedCell = modifiedModel.cells[diff.modifiedCellIndex];
const type = (originalCell.textModel?.getValue() !== modifiedCell.textModel?.getValue()) ? 'modified' : 'unchanged';
return new SideBySideDiffElementViewModel( return new SideBySideDiffElementViewModel(
model.modified.notebook, model.modified.notebook,
model.original.notebook, model.original.notebook,
originalCell, originalModel.cells[diff.originalCellIndex],
modifiedCell, modifiedModel.cells[diff.modifiedCellIndex],
type, 'unchanged', eventDispatcher,
eventDispatcher,
initData, initData,
notebookService notebookService
); );

View file

@ -419,6 +419,10 @@ export class NotebookCellTextModel extends Disposable implements ICell {
return false; return false;
} }
if (this.outputs.length !== b.outputs.length) {
return false;
}
if (this.getTextLength() !== b.getTextLength()) { if (this.getTextLength() !== b.getTextLength()) {
return false; return false;
} }

View file

@ -314,7 +314,7 @@ class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemViewModelTreeEl
const historyItemLabels = (historyItem.labels ?? []) const historyItemLabels = (historyItem.labels ?? [])
.filter(l => labels.includes(l.title)); .filter(l => labels.includes(l.title));
if (historyItemLabels) { if (historyItemLabels.length > 0) {
const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal); const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal);
const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote); const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote);
const historyItemGroupBaseColor = colorTheme.getColor(historyItemGroupBase); const historyItemGroupBaseColor = colorTheme.getColor(historyItemGroupBase);

View file

@ -117,22 +117,21 @@ export class NotebookSearchService implements INotebookSearchService {
} }
private async doesFileExist(includes: string[], folderQueries: IFolderQuery<URI>[], token: CancellationToken): Promise<boolean> { private async doesFileExist(includes: string[], folderQueries: IFolderQuery<URI>[], token: CancellationToken): Promise<boolean> {
const promises: Promise<void>[] = includes.map(async includePattern => { const promises: Promise<boolean>[] = includes.map(async includePattern => {
const query = this.queryBuilder.file(folderQueries.map(e => e.folder), { const query = this.queryBuilder.file(folderQueries.map(e => e.folder), {
includePattern: includePattern.startsWith('/') ? includePattern : '**/' + includePattern, // todo: find cleaner way to ensure that globs match all appropriate filetypes includePattern: includePattern.startsWith('/') ? includePattern : '**/' + includePattern, // todo: find cleaner way to ensure that globs match all appropriate filetypes
exists: true exists: true,
onlyFileScheme: true,
}); });
return this.searchService.fileSearch( return this.searchService.fileSearch(
query, query,
token token
).then((ret) => { ).then((ret) => {
if (!ret.limitHit) { return !!ret.limitHit;
throw Error('File not found');
}
}); });
}); });
return Promise.any(promises).then(() => true).catch(() => false); return Promise.any(promises);
} }
private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise<IClosedNotebookSearchResults> { private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise<IClosedNotebookSearchResults> {

View file

@ -328,10 +328,10 @@ __vsc_restore_exit_code() {
__vsc_prompt_cmd_original() { __vsc_prompt_cmd_original() {
__vsc_status="$?" __vsc_status="$?"
builtin local cmd
__vsc_restore_exit_code "${__vsc_status}" __vsc_restore_exit_code "${__vsc_status}"
# Evaluate the original PROMPT_COMMAND similarly to how bash would normally # Evaluate the original PROMPT_COMMAND similarly to how bash would normally
# See https://unix.stackexchange.com/a/672843 for technique # See https://unix.stackexchange.com/a/672843 for technique
builtin local cmd
for cmd in "${__vsc_original_prompt_command[@]}"; do for cmd in "${__vsc_original_prompt_command[@]}"; do
eval "${cmd:-}" eval "${cmd:-}"
done done

View file

@ -172,9 +172,9 @@ function Set-MappedKeyHandler {
function Get-KeywordCompletionResult( function Get-KeywordCompletionResult(
$Keyword, $Keyword,
$Description = $null $Description = $Keyword
) { ) {
[System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $null -ne $Description ? $Description : $Keyword) [System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $Description)
} }
function Set-MappedKeyHandlers { function Set-MappedKeyHandlers {
@ -360,7 +360,7 @@ function Send-Completions {
# completions are consistent regardless of where it was requested # completions are consistent regardless of where it was requested
elseif ($lastWord -match '[/\\]') { elseif ($lastWord -match '[/\\]') {
$lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\')) $lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\'))
if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) { if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) {
$newCursorIndex = $lastSlashIndex + 1 $newCursorIndex = $lastSlashIndex + 1
$completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex)
$prefixCursorDelta = $cursorIndex - $newCursorIndex $prefixCursorDelta = $cursorIndex - $newCursorIndex
@ -388,9 +388,9 @@ function Send-Completions {
if ($completions.CompletionMatches.Count -gt 0 -and $completions.CompletionMatches.Where({ $_.ResultType -eq 3 -or $_.ResultType -eq 4 })) { if ($completions.CompletionMatches.Count -gt 0 -and $completions.CompletionMatches.Where({ $_.ResultType -eq 3 -or $_.ResultType -eq 4 })) {
# Add `../ relative to the top completion # Add `../ relative to the top completion
$firstCompletion = $completions.CompletionMatches[0] $firstCompletion = $completions.CompletionMatches[0]
if ($firstCompletion.CompletionText.StartsWith('../')) { if ($firstCompletion.CompletionText.StartsWith("..$([System.IO.Path]::DirectorySeparatorChar)")) {
if ($completionPrefix -match '(\.\.\/)+') { if ($completionPrefix -match "(\.\.\$([System.IO.Path]::DirectorySeparatorChar))+") {
$parentDir = "$($matches[0])../" $parentDir = "$($matches[0])..$([System.IO.Path]::DirectorySeparatorChar)"
$currentPath = Split-Path -Parent $firstCompletion.ToolTip $currentPath = Split-Path -Parent $firstCompletion.ToolTip
try { try {
$parentDirPath = Split-Path -Parent $currentPath $parentDirPath = Split-Path -Parent $currentPath
@ -430,7 +430,7 @@ function Send-Completions {
# completions are consistent regardless of where it was requested # completions are consistent regardless of where it was requested
if ($completionPrefix -match '[/\\]') { if ($completionPrefix -match '[/\\]') {
$lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\')) $lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\'))
if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) { if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) {
$newCursorIndex = $lastSlashIndex + 1 $newCursorIndex = $lastSlashIndex + 1
$completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex)
$prefixCursorDelta = $cursorIndex - $newCursorIndex $prefixCursorDelta = $cursorIndex - $newCursorIndex

View file

@ -2305,7 +2305,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
async handleMouseEvent(event: MouseEvent, contextMenu: IMenu): Promise<{ cancelContextMenu: boolean } | void> { async handleMouseEvent(event: MouseEvent, contextMenu: IMenu): Promise<{ cancelContextMenu: boolean } | void> {
// Don't handle mouse event if it was on the scroll bar // Don't handle mouse event if it was on the scroll bar
if (dom.isHTMLElement(event.target) && event.target.classList.contains('scrollbar')) { if (dom.isHTMLElement(event.target) && (event.target.classList.contains('scrollbar') || event.target.classList.contains('slider'))) {
return { cancelContextMenu: true }; return { cancelContextMenu: true };
} }

View file

@ -9,6 +9,7 @@ import { AutoOpenBarrier } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isWindows } from 'vs/base/common/platform';
import { localize2 } from 'vs/nls'; import { localize2 } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey';
@ -170,16 +171,20 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
// If completions are requested, pause and queue input events until completions are // If completions are requested, pause and queue input events until completions are
// received. This fixing some problems in PowerShell, particularly enter not executing // received. This fixing some problems in PowerShell, particularly enter not executing
// when typing quickly and some characters being printed twice. // when typing quickly and some characters being printed twice. On Windows this isn't
let barrier: AutoOpenBarrier | undefined; // needed because inputs are _not_ echoed when not handled immediately.
this.add(addon.onDidRequestCompletions(() => { // TODO: This should be based on the OS of the pty host, not the client
barrier = new AutoOpenBarrier(2000); if (!isWindows) {
this._instance.pauseInputEvents(barrier); let barrier: AutoOpenBarrier | undefined;
})); this.add(addon.onDidRequestCompletions(() => {
this.add(addon.onDidReceiveCompletions(() => { barrier = new AutoOpenBarrier(2000);
barrier?.open(); this._instance.pauseInputEvents(barrier);
barrier = undefined; }));
})); this.add(addon.onDidReceiveCompletions(() => {
barrier?.open();
barrier = undefined;
}));
}
} }
} }
} }

View file

@ -30,6 +30,10 @@ export class TestResultStackWidget extends Disposable {
)); ));
} }
public collapseAll() {
this.widget.collapseAll();
}
public update(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) { public update(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) {
this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame( this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame(
frame.label, frame.label,

View file

@ -22,6 +22,9 @@ interface ISubjectCommon {
controllerId: string; controllerId: string;
} }
export const inspectSubjectHasStack = (subject: InspectSubject | undefined) =>
subject instanceof MessageSubject && !!subject.stack?.length;
export class MessageSubject implements ISubjectCommon { export class MessageSubject implements ISubjectCommon {
public readonly test: ITestItem; public readonly test: ITestItem;
public readonly message: ITestMessage; public readonly message: ITestMessage;

View file

@ -838,22 +838,21 @@ class TreeActionsProvider {
} }
if (element instanceof TestMessageElement) { if (element instanceof TestMessageElement) {
id = MenuId.TestMessageContext;
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);
primary.push(new Action( primary.push(new Action(
'testing.outputPeek.goToFile', 'testing.outputPeek.goToTest',
localize('testing.goToFile', "Go to Source"), localize('testing.goToTest', "Go to Test"),
ThemeIcon.asClassName(Codicon.goToFile), ThemeIcon.asClassName(Codicon.goToFile),
undefined, undefined,
() => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId),
)); ));
}
if (element instanceof TestMessageElement) {
id = MenuId.TestMessageContext;
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);
if (this.showRevealLocationOnMessages && element.location) { if (this.showRevealLocationOnMessages && element.location) {
primary.push(new Action( primary.push(new Action(
'testing.outputPeek.goToError', 'testing.outputPeek.goToError',
localize('testing.goToError', "Go to Source"), localize('testing.goToError', "Go to Error"),
ThemeIcon.asClassName(Codicon.goToFile), ThemeIcon.asClassName(Codicon.goToFile),
undefined, undefined,
() => this.editorService.openEditor({ () => this.editorService.openEditor({

View file

@ -322,6 +322,13 @@ export class TestResultsViewContent extends Disposable {
}); });
} }
/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.callStackWidget.collapseAll();
}
private getCallFrames(subject: InspectSubject) { private getCallFrames(subject: InspectSubject) {
if (!(subject instanceof MessageSubject)) { if (!(subject instanceof MessageSubject)) {
return undefined; return undefined;

View file

@ -25,7 +25,7 @@ import { testingResultsIcon, testingViewIcon } from 'vs/workbench/contrib/testin
import { TestCoverageView } from 'vs/workbench/contrib/testing/browser/testCoverageView'; import { TestCoverageView } from 'vs/workbench/contrib/testing/browser/testCoverageView';
import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { CloseTestPeek, CollapsePeekStack, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
@ -136,6 +136,7 @@ registerAction2(GoToPreviousMessageAction);
registerAction2(GoToNextMessageAction); registerAction2(GoToNextMessageAction);
registerAction2(CloseTestPeek); registerAction2(CloseTestPeek);
registerAction2(ToggleTestingPeekHistory); registerAction2(ToggleTestingPeekHistory);
registerAction2(CollapsePeekStack);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually); Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually);

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom'; import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
import { equals } from 'vs/base/common/arrays'; import { equals } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async'; import { RunOnceScheduler } from 'vs/base/common/async';
@ -456,11 +457,24 @@ export class TestingDecorations extends Disposable implements IEditorContributio
decorations.syncDecorations(this._currentUri); decorations.syncDecorations(this._currentUri);
} }
})); }));
this._register(this.editor.onKeyDown(e => {
if (e.keyCode === KeyCode.Alt && this._currentUri) { const win = dom.getWindow(editor.getDomNode());
decorations.updateDecorationsAlternateAction(this._currentUri!, true); this._register(dom.addDisposableListener(win, 'keydown', e => {
if (new StandardKeyboardEvent(e).keyCode === KeyCode.Alt && this._currentUri) {
decorations.updateDecorationsAlternateAction(this._currentUri, true);
} }
})); }));
this._register(dom.addDisposableListener(win, 'keyup', e => {
if (new StandardKeyboardEvent(e).keyCode === KeyCode.Alt && this._currentUri) {
decorations.updateDecorationsAlternateAction(this._currentUri, false);
}
}));
this._register(dom.addDisposableListener(win, 'blur', () => {
if (this._currentUri) {
decorations.updateDecorationsAlternateAction(this._currentUri, false);
}
}));
this._register(this.editor.onKeyUp(e => { this._register(this.editor.onKeyUp(e => {
if (e.keyCode === KeyCode.Alt && this._currentUri) { if (e.keyCode === KeyCode.Alt && this._currentUri) {
decorations.updateDecorationsAlternateAction(this._currentUri!, false); decorations.updateDecorationsAlternateAction(this._currentUri!, false);

View file

@ -14,6 +14,7 @@ import { Iterable } from 'vs/base/common/iterator';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Lazy } from 'vs/base/common/lazy'; import { Lazy } from 'vs/base/common/lazy';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { observableValue } from 'vs/base/common/observable';
import { count } from 'vs/base/common/strings'; import { count } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
@ -43,6 +44,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -52,7 +54,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IViewDescriptorService } from 'vs/workbench/common/views';
import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer';
import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject, mapFindTestMessage } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject, inspectSubjectHasStack, mapFindTestMessage } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject';
import { TestResultsViewContent } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent'; import { TestResultsViewContent } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent';
import { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme'; import { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { AutoOpenPeekViewWhen, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
@ -498,6 +500,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
this.peek.clear(); this.peek.clear();
} }
/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.peek.value?.collapseStack();
}
/** /**
* Shows the next message in the peek, if possible. * Shows the next message in the peek, if possible.
*/ */
@ -645,10 +654,14 @@ class TestResultsPeek extends PeekViewWidget {
private static lastHeightInLines?: number; private static lastHeightInLines?: number;
private readonly visibilityChange = this._disposables.add(new Emitter<boolean>()); private readonly visibilityChange = this._disposables.add(new Emitter<boolean>());
private readonly _current = observableValue<InspectSubject | undefined>('testPeekCurrent', undefined);
private content!: TestResultsViewContent; private content!: TestResultsViewContent;
private scopedContextKeyService!: IContextKeyService; private scopedContextKeyService!: IContextKeyService;
private dimension?: dom.Dimension; private dimension?: dom.Dimension;
public current?: InspectSubject;
public get current() {
return this._current.get();
}
constructor( constructor(
editor: ICodeEditor, editor: ICodeEditor,
@ -702,7 +715,14 @@ class TestResultsPeek extends PeekViewWidget {
protected override _fillHead(container: HTMLElement): void { protected override _fillHead(container: HTMLElement): void {
super._fillHead(container); super._fillHead(container);
const menu = this.menuService.createMenu(MenuId.TestPeekTitle, this.contextKeyService); const menuContextKeyService = this._disposables.add(this.contextKeyService.createScoped(container));
this._disposables.add(bindContextKey(
TestingContextKeys.peekHasStack,
menuContextKeyService,
reader => inspectSubjectHasStack(this._current.read(reader)),
));
const menu = this.menuService.createMenu(MenuId.TestPeekTitle, menuContextKeyService);
const actionBar = this._actionbarWidget!; const actionBar = this._actionbarWidget!;
this._disposables.add(menu.onDidChange(() => { this._disposables.add(menu.onDidChange(() => {
actions.length = 0; actions.length = 0;
@ -732,7 +752,7 @@ class TestResultsPeek extends PeekViewWidget {
*/ */
public setModel(subject: InspectSubject): Promise<void> { public setModel(subject: InspectSubject): Promise<void> {
if (subject instanceof TaskSubject || subject instanceof TestOutputSubject) { if (subject instanceof TaskSubject || subject instanceof TestOutputSubject) {
this.current = subject; this._current.set(subject, undefined);
return this.showInPlace(subject); return this.showInPlace(subject);
} }
@ -743,14 +763,14 @@ class TestResultsPeek extends PeekViewWidget {
return Promise.resolve(); return Promise.resolve();
} }
this.current = subject; this._current.set(subject, undefined);
if (!revealLocation) { if (!revealLocation) {
return this.showInPlace(subject); return this.showInPlace(subject);
} }
// If there is a stack we want to display, ensure the default size is large-ish // If there is a stack we want to display, ensure the default size is large-ish
const peekLines = TestResultsPeek.lastHeightInLines || Math.max( const peekLines = TestResultsPeek.lastHeightInLines || Math.max(
subject instanceof MessageSubject && subject.stack?.length ? Math.ceil(this.getVisibleEditorLines() / 2) : 0, inspectSubjectHasStack(subject) ? Math.ceil(this.getVisibleEditorLines() / 2) : 0,
hintMessagePeekHeight(message) hintMessagePeekHeight(message)
); );
@ -760,6 +780,13 @@ class TestResultsPeek extends PeekViewWidget {
return this.showInPlace(subject); return this.showInPlace(subject);
} }
/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.content.collapseStack();
}
private getVisibleEditorLines() { private getVisibleEditorLines() {
// note that we don't use the view ranges because we don't want to get // note that we don't use the view ranges because we don't want to get
// thrown off by large wrapping lines. Being approximate here is okay. // thrown off by large wrapping lines. Being approximate here is okay.
@ -1025,6 +1052,31 @@ export class GoToPreviousMessageAction extends Action2 {
} }
} }
export class CollapsePeekStack extends Action2 {
public static readonly ID = 'testing.collapsePeekStack';
constructor() {
super({
id: CollapsePeekStack.ID,
title: localize2('testing.collapsePeekStack', 'Collapse Stack Frames'),
icon: Codicon.collapseAll,
category: Categories.Test,
menu: [{
id: MenuId.TestPeekTitle,
when: TestingContextKeys.peekHasStack,
group: 'navigation',
order: 4,
}],
});
}
public override run(accessor: ServicesAccessor) {
const editor = getPeekedEditorFromFocus(accessor.get(ICodeEditorService));
if (editor) {
TestingOutputPeekController.get(editor)?.collapseStack();
}
}
}
export class OpenMessageInEditorAction extends Action2 { export class OpenMessageInEditorAction extends Action2 {
public static readonly ID = 'testing.openMessageInEditor'; public static readonly ID = 'testing.openMessageInEditor';
constructor() { constructor() {

View file

@ -29,6 +29,7 @@ export namespace TestingContextKeys {
export const inlineCoverageEnabled = new RawContextKey('testing.inlineCoverageEnabled', false, { type: 'boolean', description: localize('testing.inlineCoverageEnabled', 'Indicates whether inline coverage is shown') }); export const inlineCoverageEnabled = new RawContextKey('testing.inlineCoverageEnabled', false, { type: 'boolean', description: localize('testing.inlineCoverageEnabled', 'Indicates whether inline coverage is shown') });
export const canGoToRelatedCode = new RawContextKey('testing.canGoToRelatedCode', false, { type: 'boolean', description: localize('testing.canGoToRelatedCode', 'Whether a controller implements a capability to find code related to a test') }); export const canGoToRelatedCode = new RawContextKey('testing.canGoToRelatedCode', false, { type: 'boolean', description: localize('testing.canGoToRelatedCode', 'Whether a controller implements a capability to find code related to a test') });
export const canGoToRelatedTest = new RawContextKey('testing.canGoToRelatedTest', false, { type: 'boolean', description: localize('testing.canGoToRelatedTest', 'Whether a controller implements a capability to find tests related to code') }); export const canGoToRelatedTest = new RawContextKey('testing.canGoToRelatedTest', false, { type: 'boolean', description: localize('testing.canGoToRelatedTest', 'Whether a controller implements a capability to find tests related to code') });
export const peekHasStack = new RawContextKey('testing.peekHasStack', false, { type: 'boolean', description: localize('testing.peekHasStack', 'Whether the message shown in a peek view has a stack trace') });
export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey<boolean> } = { export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey<boolean> } = {
[TestRunProfileBitset.Run]: hasRunnableTests, [TestRunProfileBitset.Run]: hasRunnableTests,

View file

@ -92,6 +92,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
private profileWidget: ProfileWidget | undefined; private profileWidget: ProfileWidget | undefined;
private model: UserDataProfilesEditorModel | undefined; private model: UserDataProfilesEditorModel | undefined;
private templates: readonly IProfileTemplateInfo[] = [];
constructor( constructor(
group: IEditorGroup, group: IEditorGroup,
@ -207,7 +208,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
actions: { actions: {
getActions: () => { getActions: () => {
const actions: IAction[] = []; const actions: IAction[] = [];
if (this.model?.templates.length) { if (this.templates.length) {
actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions()));
actions.push(new Separator()); actions.push(new Separator());
} }
@ -225,15 +226,13 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
} }
private getCreateFromTemplateActions(): IAction[] { private getCreateFromTemplateActions(): IAction[] {
return this.model return this.templates.map(template =>
? this.model.templates.map(template => new Action(
new Action( `template:${template.url}`,
`template:${template.url}`, template.name,
template.name, undefined,
undefined, true,
true, () => this.createNewProfile(URI.parse(template.url))));
() => this.createNewProfile(URI.parse(template.url))))
: [];
} }
private registerListeners(): void { private registerListeners(): void {
@ -343,9 +342,12 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token); await super.setInput(input, options, context, token);
this.model = await input.resolve(); this.model = await input.resolve();
if (this.profileWidget) { this.model.getTemplates().then(templates => {
this.profileWidget.templates = this.model.templates; this.templates = templates;
} if (this.profileWidget) {
this.profileWidget.templates = templates;
}
});
this.updateProfilesList(); this.updateProfilesList();
this._register(this.model.onDidChange(element => this._register(this.model.onDidChange(element =>
this.updateProfilesList(element))); this.updateProfilesList(element)));
@ -710,7 +712,6 @@ class ProfileTreeDataSource implements IAsyncDataSource<AbstractUserDataProfileE
children.push({ element: 'name', root: element }); children.push({ element: 'name', root: element });
children.push({ element: 'icon', root: element }); children.push({ element: 'icon', root: element });
} }
children.push({ element: 'useForCurrent', root: element });
children.push({ element: 'useAsDefault', root: element }); children.push({ element: 'useAsDefault', root: element });
children.push({ element: 'contents', root: element }); children.push({ element: 'contents', root: element });
} }

View file

@ -713,8 +713,7 @@ export class UserDataProfilesEditorModel extends EditorModel {
private _onDidChange = this._register(new Emitter<AbstractUserDataProfileElement | undefined>()); private _onDidChange = this._register(new Emitter<AbstractUserDataProfileElement | undefined>());
readonly onDidChange = this._onDidChange.event; readonly onDidChange = this._onDidChange.event;
private _templates: IProfileTemplateInfo[] | undefined; private templates: Promise<readonly IProfileTemplateInfo[]> | undefined;
get templates(): readonly IProfileTemplateInfo[] { return this._templates ?? []; }
constructor( constructor(
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@ -761,9 +760,11 @@ export class UserDataProfilesEditorModel extends EditorModel {
} }
} }
override async resolve(): Promise<void> { getTemplates(): Promise<readonly IProfileTemplateInfo[]> {
await super.resolve(); if (!this.templates) {
this._templates = await this.userDataProfileManagementService.getBuiltinProfileTemplates(); this.templates = this.userDataProfileManagementService.getBuiltinProfileTemplates();
}
return this.templates;
} }
private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] {
@ -771,7 +772,7 @@ export class UserDataProfilesEditorModel extends EditorModel {
const activateAction = disposables.add(new Action( const activateAction = disposables.add(new Action(
'userDataProfile.activate', 'userDataProfile.activate',
localize('active', "Use for Current Window"), localize('active', "Use this Profile for Current Window"),
ThemeIcon.asClassName(Codicon.check), ThemeIcon.asClassName(Codicon.check),
true, true,
() => this.userDataProfileManagementService.switchProfile(profileElement.profile) () => this.userDataProfileManagementService.switchProfile(profileElement.profile)
@ -808,25 +809,16 @@ export class UserDataProfilesEditorModel extends EditorModel {
() => this.openWindow(profileElement.profile) () => this.openWindow(profileElement.profile)
)); ));
const useAsNewWindowProfileAction = disposables.add(new Action(
'userDataProfile.useAsNewWindowProfile',
localize('use as new window', "Use for New Windows"),
undefined,
true,
() => profileElement.toggleNewWindowProfile()
));
const primaryActions: IAction[] = []; const primaryActions: IAction[] = [];
primaryActions.push(activateAction);
primaryActions.push(newWindowAction); primaryActions.push(newWindowAction);
if (!profile.isDefault) {
primaryActions.push(deleteAction);
}
const secondaryActions: IAction[] = []; const secondaryActions: IAction[] = [];
secondaryActions.push(activateAction);
secondaryActions.push(useAsNewWindowProfileAction);
secondaryActions.push(new Separator());
secondaryActions.push(copyFromProfileAction); secondaryActions.push(copyFromProfileAction);
secondaryActions.push(exportAction); secondaryActions.push(exportAction);
if (!profile.isDefault) {
secondaryActions.push(new Separator());
secondaryActions.push(deleteAction);
}
const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement,
profile, profile,
@ -834,16 +826,9 @@ export class UserDataProfilesEditorModel extends EditorModel {
[primaryActions, secondaryActions] [primaryActions, secondaryActions]
)); ));
activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id; activateAction.enabled = this.userDataProfileService.currentProfile.id !== profileElement.profile.id;
disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() =>
activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id)); activateAction.enabled = this.userDataProfileService.currentProfile.id !== profileElement.profile.id));
useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile;
disposables.add(profileElement.onDidChange(e => {
if (e.newWindowProfile) {
useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile;
}
}));
return [profileElement, disposables]; return [profileElement, disposables];
} }

View file

@ -226,6 +226,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
'onSettingChanged:workbench.colorTheme', 'onSettingChanged:workbench.colorTheme',
'onCommand:workbench.action.selectTheme' 'onCommand:workbench.action.selectTheme'
], ],
when: '!accessibilityModeEnabled',
media: { type: 'markdown', path: 'theme_picker', } media: { type: 'markdown', path: 'theme_picker', }
}, },
{ {
@ -399,7 +400,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
isFeatured: true, isFeatured: true,
icon: setupIcon, icon: setupIcon,
when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key, when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key,
next: 'SetupScreenReaderExtended', next: 'Setup',
content: { content: {
type: 'steps', type: 'steps',
steps: [ steps: [
@ -470,90 +471,6 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
] ]
} }
}, },
{
id: 'SetupScreenReaderExtended',
title: localize('gettingStarted.setupScreenReaderExtended.title', "Learn more about using VS Code with a Screen Reader"),
description: localize('gettingStarted.setupScreenReaderExtended.description', "Customize your editor, learn the basics, and start coding"),
isFeatured: true,
icon: setupIcon,
when: `!isWeb && ${CONTEXT_ACCESSIBILITY_MODE_ENABLED.key}`,
content: {
type: 'steps',
steps: [
{
id: 'extensionsWeb',
title: localize('gettingStarted.extensions.title', "Code with extensions"),
description: localize('gettingStarted.extensionsWeb.description.interpolated', "Extensions are VS Code's power-ups. A growing number are becoming available in the web.\n{0}", Button(localize('browsePopularWeb', "Browse Popular Web Extensions"), 'command:workbench.extensions.action.showPopularExtensions')),
when: 'workspacePlatform == \'webworker\'',
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'findLanguageExtensions',
title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"),
description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')),
when: 'workspacePlatform != \'webworker\'',
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'settings',
title: localize('gettingStarted.settings.title', "Tune your settings"),
description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')),
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'settingsSync',
title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"),
description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')),
when: 'syncStatus != uninitialized',
completionEvents: ['onEvent:sync-enabled'],
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'commandPaletteTask',
title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "),
description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')),
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'pickAFolderTask-Mac',
title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"),
description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')),
when: 'isMac && workspaceFolderCount == 0',
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'pickAFolderTask-Other',
title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"),
description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')),
when: '!isMac && workspaceFolderCount == 0',
media: {
type: 'markdown', path: 'empty'
}
},
{
id: 'quickOpen',
title: localize('gettingStarted.quickOpen.title', "Quickly navigate between your files"),
description: localize('gettingStarted.quickOpen.description.interpolated', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", Button(localize('quickOpen', "Quick Open a File"), 'command:toSide:workbench.action.quickOpen')),
when: 'workspaceFolderCount != 0',
media: {
type: 'markdown', path: 'empty'
}
},
]
}
},
{ {
id: 'Beginner', id: 'Beginner',
isFeatured: false, isFeatured: false,

View file

@ -28,7 +28,6 @@ import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
import { ModifierKeyEmitter } from 'vs/base/browser/dom'; import { ModifierKeyEmitter } from 'vs/base/browser/dom';
import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from 'vs/workbench/common/configuration';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sandbox/window'; import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sandbox/window';
import product from 'vs/platform/product/common/product';
// Actions // Actions
(function registerActions(): void { (function registerActions(): void {
@ -240,7 +239,7 @@ import product from 'vs/platform/product/common/product';
'type': 'boolean', 'type': 'boolean',
'included': isLinux, 'included': isLinux,
'markdownDescription': localize('window.experimentalControlOverlay', "Show the native window controls when {0} is set to `custom` (Linux only).", '`#window.titleBarStyle#`'), 'markdownDescription': localize('window.experimentalControlOverlay', "Show the native window controls when {0} is set to `custom` (Linux only).", '`#window.titleBarStyle#`'),
'default': product.quality !== 'stable', // TODO@bpasero disable by default in stable for now (TODO@bpasero TODO@benibenj flip when custom title is default) 'default': true
}, },
'window.customTitleBarVisibility': { 'window.customTitleBarVisibility': {
'type': 'string', 'type': 'string',

View file

@ -156,17 +156,17 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
}))); })));
} }
// Window Controls (Native Windows/Linux) // Window Controls (Native Linux when WCO is disabled)
if (!isMacintosh && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.primaryWindowControls) { if (isLinux && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.windowControlsContainer) {
// Minimize // Minimize
const minimizeIcon = append(this.primaryWindowControls, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize))); const minimizeIcon = append(this.windowControlsContainer, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize)));
this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => { this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => {
this.nativeHostService.minimizeWindow({ targetWindowId }); this.nativeHostService.minimizeWindow({ targetWindowId });
})); }));
// Restore // Restore
this.maxRestoreControl = append(this.primaryWindowControls, $('div.window-icon.window-max-restore')); this.maxRestoreControl = append(this.windowControlsContainer, $('div.window-icon.window-max-restore'));
this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => { this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => {
const maximized = await this.nativeHostService.isMaximized({ targetWindowId }); const maximized = await this.nativeHostService.isMaximized({ targetWindowId });
if (maximized) { if (maximized) {
@ -177,7 +177,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
})); }));
// Close // Close
const closeIcon = append(this.primaryWindowControls, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose))); const closeIcon = append(this.windowControlsContainer, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose)));
this._register(addDisposableListener(closeIcon, EventType.CLICK, () => { this._register(addDisposableListener(closeIcon, EventType.CLICK, () => {
this.nativeHostService.closeWindow({ targetWindowId }); this.nativeHostService.closeWindow({ targetWindowId });
})); }));

View file

@ -41,7 +41,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
public readonly onEnablementChanged: Event<readonly IExtension[]> = this._onEnablementChanged.event; public readonly onEnablementChanged: Event<readonly IExtension[]> = this._onEnablementChanged.event;
protected readonly extensionsManager: ExtensionsManager; protected readonly extensionsManager: ExtensionsManager;
private readonly storageManger: StorageManager; private readonly storageManager: StorageManager;
constructor( constructor(
@IStorageService storageService: IStorageService, @IStorageService storageService: IStorageService,
@ -63,7 +63,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
@IInstantiationService instantiationService: IInstantiationService, @IInstantiationService instantiationService: IInstantiationService,
) { ) {
super(); super();
this.storageManger = this._register(new StorageManager(storageService)); this.storageManager = this._register(new StorageManager(storageService));
const uninstallDisposable = this._register(Event.filter(extensionManagementService.onDidUninstallExtension, e => !e.error)(({ identifier }) => this._reset(identifier))); const uninstallDisposable = this._register(Event.filter(extensionManagementService.onDidUninstallExtension, e => !e.error)(({ identifier }) => this._reset(identifier)));
let isDisposed = false; let isDisposed = false;
@ -610,11 +610,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
if (!this.hasWorkspace) { if (!this.hasWorkspace) {
return []; return [];
} }
return this.storageManger.get(storageId, StorageScope.WORKSPACE); return this.storageManager.get(storageId, StorageScope.WORKSPACE);
} }
private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void { private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void {
this.storageManger.set(storageId, extensions, StorageScope.WORKSPACE); this.storageManager.set(storageId, extensions, StorageScope.WORKSPACE);
} }
private async _onDidChangeGloballyDisabledExtensions(extensionIdentifiers: ReadonlyArray<IExtensionIdentifier>, source?: string): Promise<void> { private async _onDidChangeGloballyDisabledExtensions(extensionIdentifiers: ReadonlyArray<IExtensionIdentifier>, source?: string): Promise<void> {

View file

@ -13,6 +13,8 @@ import { URI } from 'vs/base/common/uri';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search';
import { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes'; import { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes';
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { Disposable } from 'vs/base/common/lifecycle';
import { OldFileSearchProviderConverter } from 'vs/workbench/services/search/common/searchExtConversionTypes';
interface IInternalFileMatch { interface IInternalFileMatch {
base: URI; base: URI;
@ -53,7 +55,7 @@ class FileSearchEngine {
private globalExcludePattern?: glob.ParsedExpression; private globalExcludePattern?: glob.ParsedExpression;
constructor(private config: IFileQuery, private provider: FileSearchProviderNew, private sessionToken?: unknown) { constructor(private config: IFileQuery, private provider: FileSearchProviderNew, private sessionLifecycle?: SessionLifecycle) {
this.filePattern = config.filePattern; this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern); this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || undefined; this.maxResults = config.maxResults || undefined;
@ -116,10 +118,11 @@ class FileSearchEngine {
private async doSearch(fqs: IFolderQuery<URI>[], onResult: (match: IInternalFileMatch) => void): Promise<IFileSearchProviderStats | null> { private async doSearch(fqs: IFolderQuery<URI>[], onResult: (match: IInternalFileMatch) => void): Promise<IFileSearchProviderStats | null> {
const cancellation = new CancellationTokenSource(); const cancellation = new CancellationTokenSource();
const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq)); const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq));
const session = this.provider instanceof OldFileSearchProviderConverter ? this.sessionLifecycle?.tokenSource.token : this.sessionLifecycle?.obj;
const options: FileSearchProviderOptions = { const options: FileSearchProviderOptions = {
folderOptions, folderOptions,
maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS,
session: this.sessionToken session
}; };
@ -301,11 +304,30 @@ interface IInternalSearchComplete {
stats?: IFileSearchProviderStats; stats?: IFileSearchProviderStats;
} }
/**
* For backwards compatibility, store both a cancellation token and a session object. The session object is the new implementation, where
*/
class SessionLifecycle extends Disposable {
public readonly obj: object;
public readonly tokenSource: CancellationTokenSource;
constructor() {
super();
this.obj = new Object();
this.tokenSource = new CancellationTokenSource();
}
public override dispose(): void {
this.tokenSource.cancel();
super.dispose();
}
}
export class FileSearchManager { export class FileSearchManager {
private static readonly BATCH_SIZE = 512; private static readonly BATCH_SIZE = 512;
private readonly sessions = new Map<string, unknown>(); private readonly sessions = new Map<string, SessionLifecycle>();
fileSearch(config: IFileQuery, provider: FileSearchProviderNew, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> { fileSearch(config: IFileQuery, provider: FileSearchProviderNew, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey); const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
@ -333,17 +355,19 @@ export class FileSearchManager {
} }
clearCache(cacheKey: string): void { clearCache(cacheKey: string): void {
// cancel the token
this.sessions.get(cacheKey)?.dispose();
// with no reference to this, it will be removed from WeakMaps // with no reference to this, it will be removed from WeakMaps
this.sessions.delete(cacheKey); this.sessions.delete(cacheKey);
} }
private getSessionTokenSource(cacheKey: string | undefined): unknown { private getSessionTokenSource(cacheKey: string | undefined): SessionLifecycle | undefined {
if (!cacheKey) { if (!cacheKey) {
return undefined; return undefined;
} }
if (!this.sessions.has(cacheKey)) { if (!this.sessions.has(cacheKey)) {
this.sessions.set(cacheKey, new Object()); this.sessions.set(cacheKey, new SessionLifecycle());
} }
return this.sessions.get(cacheKey); return this.sessions.get(cacheKey);

View file

@ -92,6 +92,7 @@ interface ICommonQueryBuilderOptions {
disregardSearchExcludeSettings?: boolean; disregardSearchExcludeSettings?: boolean;
ignoreSymlinks?: boolean; ignoreSymlinks?: boolean;
onlyOpenEditors?: boolean; onlyOpenEditors?: boolean;
onlyFileScheme?: boolean;
} }
export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions { export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions {
@ -260,7 +261,8 @@ export class QueryBuilder {
excludePattern: excludeSearchPathsInfo.pattern, excludePattern: excludeSearchPathsInfo.pattern,
includePattern: includeSearchPathsInfo.pattern, includePattern: includeSearchPathsInfo.pattern,
onlyOpenEditors: options.onlyOpenEditors, onlyOpenEditors: options.onlyOpenEditors,
maxResults: options.maxResults maxResults: options.maxResults,
onlyFileScheme: options.onlyFileScheme
}; };
if (options.onlyOpenEditors) { if (options.onlyOpenEditors) {

View file

@ -100,6 +100,7 @@ export interface ICommonQueryProps<U extends UriComponents> {
maxResults?: number; maxResults?: number;
usingSearchPaths?: boolean; usingSearchPaths?: boolean;
onlyFileScheme?: boolean;
} }
export interface IFileQueryProps<U extends UriComponents> extends ICommonQueryProps<U> { export interface IFileQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {

View file

@ -271,6 +271,9 @@ export class SearchService extends Disposable implements ISearchService {
return []; return [];
} }
await Promise.all([...fqs.keys()].map(async scheme => { await Promise.all([...fqs.keys()].map(async scheme => {
if (query.onlyFileScheme && scheme !== Schemas.file) {
return;
}
const schemeFQs = fqs.get(scheme)!; const schemeFQs = fqs.get(scheme)!;
let provider = this.getSearchProvider(query.type).get(scheme); let provider = this.getSearchProvider(query.type).get(scheme);

View file

@ -143,7 +143,7 @@ export class TestNativeHostService implements INativeHostService {
async closeWindow(): Promise<void> { } async closeWindow(): Promise<void> { }
async quit(): Promise<void> { } async quit(): Promise<void> { }
async exit(code: number): Promise<void> { } async exit(code: number): Promise<void> { }
async openDevTools(options?: Partial<Electron.OpenDevToolsOptions> & INativeHostOptions | undefined): Promise<void> { } async openDevTools(): Promise<void> { }
async toggleDevTools(): Promise<void> { } async toggleDevTools(): Promise<void> { }
async resolveProxy(url: string): Promise<string | undefined> { return undefined; } async resolveProxy(url: string): Promise<string | undefined> { return undefined; }
async lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined> { return undefined; } async lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined> { return undefined; }

View file

@ -3,11 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
//#region --- void
import 'vs/workbench/contrib/void/browser/void.contribution'
//#endregion
//#region --- editor/workbench core //#region --- editor/workbench core
import 'vs/editor/editor.all'; import 'vs/editor/editor.all';
@ -15,7 +10,6 @@ import 'vs/editor/editor.all';
import 'vs/workbench/api/browser/extensionHost.contribution'; import 'vs/workbench/api/browser/extensionHost.contribution';
import 'vs/workbench/browser/workbench.contribution'; import 'vs/workbench/browser/workbench.contribution';
//#endregion //#endregion