Merge branch 'main' into feat/use_npm

This commit is contained in:
quant-eagle 2024-09-19 20:04:04 +02:00 committed by GitHub
commit 3b440a4653
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 1040 additions and 581 deletions

View file

@ -4,10 +4,8 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a
There are two main ways to contribute:
- Suggest New Features (discord)
- Build New Features (roadmap)
See the [GitHub Issues](https://github.com/orgs/voideditor/projects/2/views/3) list for a list of the most important features to build, or feel free to create new issues.
- Suggest New Features ([discord](https://discord.gg/4GAxHVAD))
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
@ -44,7 +42,7 @@ npm run build
This will start a new instance of VS Code with the extension enabled. If this does not work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VS Code Extension Development".
If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) and modifying `void > "Anthropic Api Key"`. The "Which API" environment variable controls the provider and defaults to "anthropic".
If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) typing in "void", and adding the API key you want to use (eg. the `"Anthropic Api Key"` environment variable). The "Which API" environment variable controls the provider and defaults to "anthropic".
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
@ -52,7 +50,11 @@ Now that you're set up, feel free to check out our [Issues](https://github.com/v
Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
1. Install all dependencies. Make sure you have node installed.
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`).
Make sure you're on the correct NodeJS version as per `.nvmrc`.
1. Install all dependencies.
```
npm ci

View file

@ -136,6 +136,6 @@ export function isWCOEnabled(): boolean {
// 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
export function getWCOBoundingRect(): DOMRect | undefined {
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
export function getWCOTitlebarAreaRect(targetWindow: Window): DOMRect | undefined {
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 { Button } from 'vs/base/browser/ui/button/button';
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 {
readonly activeForeground?: string;
@ -53,7 +53,7 @@ export class Radio extends Widget {
constructor(opts: IRadioOptions) {
super();
this.hoverDelegate = opts.hoverDelegate ?? getDefaultHoverDelegate('element');
this.hoverDelegate = opts.hoverDelegate ?? this._register(createInstantHoverDelegate());
this.domNode = $('.monaco-custom-radio');
this.domNode.setAttribute('role', 'radio');

View file

@ -217,24 +217,6 @@ export interface FileFilter {
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 {
// 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
if (hideOnHover && options.appearance?.showHoverHint) {
if (options.appearance?.showHoverHint) {
const statusBarElement = $('div.hover-row.status-bar');
const infoElement = $('div.info');
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 {
range: IRange; // the range of code
id?: string; // no idea what this is for
command?: Command; // command to run when they click the codeLens
range: IRange;
id?: string;
command?: Command;
}
export interface CodeLensList {

View file

@ -180,6 +180,12 @@ export class CodeActionController extends Disposable implements IEditorContribut
return;
}
const selection = this._editor.getSelection();
if (selection?.startLineNumber !== newState.position.lineNumber) {
return;
}
this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position);
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 _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 _quickFixKbLabel?: string;
@ -148,15 +156,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
}));
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;
}
@ -247,7 +248,9 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
let hasDecoration = false;
if (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;
break;
}
@ -271,7 +274,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber);
const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented;
// check above and below. if both are blocked, display lightbulb in the gutter.
if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) {
this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, {
@ -280,7 +282,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
});
this.renderGutterLightbub();
return this.hide();
} else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) {
} else if (prevLineEmptyOrIndented || endLine || (prevLineEmptyOrIndented && !currLineEmptyOrIndented)) {
effectiveLineNumber -= 1;
} else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) {
effectiveLineNumber += 1;

View file

@ -3,17 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../../base/common/event.js';
import { LRUCache } from '../../../../base/common/map.js';
import { Range } from '../../../common/core/range.js';
import { ITextModel } from '../../../common/model.js';
import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js';
import { CodeLensModel } from './codelens.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { runWhenWindowIdle } from '../../../../base/browser/dom.js';
import { Event } from 'vs/base/common/event';
import { LRUCache } from 'vs/base/common/map';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages';
import { CodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { mainWindow } from 'vs/base/browser/window';
import { runWhenWindowIdle } from 'vs/base/browser/dom';
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.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { assertType } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { ITextModel } from '../../../common/model.js';
import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js';
import { IModelService } from '../../../common/services/model.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { CancellationToken } from 'vs/base/common/cancellation';
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages';
import { IModelService } from 'vs/editor/common/services/model';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
export interface CodeLensItem {
symbol: CodeLens;

View file

@ -4,26 +4,26 @@
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js';
import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js';
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';
import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from '../../../browser/editorBrowser.js';
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';
import { EditorOption, EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js';
import { IEditorContribution } from '../../../common/editorCommon.js';
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
import { IModelDecorationsChangeAccessor } from '../../../common/model.js';
import { CodeLens, Command } from '../../../common/languages.js';
import { CodeLensItem, CodeLensModel, getCodeLensModel } from './codelens.js';
import { ICodeLensCache } from './codeLensCache.js';
import { CodeLensHelper, CodeLensWidget } from './codelensWidget.js';
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from 'vs/base/common/async';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
import { CodeLens, Command } from 'vs/editor/common/languages';
import { CodeLensItem, CodeLensModel, getCodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens';
import { ICodeLensCache } from 'vs/editor/contrib/codelens/browser/codeLensCache';
import { CodeLensHelper, CodeLensWidget } from 'vs/editor/contrib/codelens/browser/codelensWidget';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
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.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../base/browser/dom.js';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { Constants } from '../../../../base/common/uint.js';
import './codelensWidget.css';
import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from '../../../browser/editorBrowser.js';
import { Range } from '../../../common/core/range.js';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from '../../../common/model.js';
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
import { CodeLens, Command } from '../../../common/languages.js';
import { CodeLensItem } from './codelens.js';
import * as dom from 'vs/base/browser/dom';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Constants } from 'vs/base/common/uint';
import 'vs/css!./codelensWidget';
import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { CodeLens, Command } from 'vs/editor/common/languages';
import { CodeLensItem } from 'vs/editor/contrib/codelens/browser/codelens';
class CodeLensViewZone implements IViewZone {

View file

@ -318,7 +318,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable
}
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) {
// cancel rename when input widget isn't rendered anymore
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 {
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);
}

View file

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

View file

@ -132,8 +132,9 @@
/* 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);
margin-top: 2px;
}
.action-widget .action-widget-action-bar::before {
@ -143,7 +144,7 @@
}
.action-widget .action-widget-action-bar .actions-container {
padding: 0 8px;
padding: 3px 8px 0;
}
.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 }>();
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }> = this._onDidChangeEnablement.event;
private readonly storageManger: StorageManager;
private readonly storageManager: StorageManager;
constructor(
@IStorageService storageService: IStorageService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
) {
super();
this.storageManger = this._register(new StorageManager(storageService));
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
this.storageManager = this._register(new StorageManager(storageService));
this._register(this.storageManager.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => {
if (local && operation === InstallOperation.Migrate) {
this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */
@ -84,11 +84,11 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
}
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 {
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 { Event } from 'vs/base/common/event';
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 { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@ -178,7 +178,7 @@ export interface ICommonNativeHostService {
exit(code: number): Promise<void>;
// Development
openDevTools(options?: Partial<OpenDevToolsOptions> & INativeHostOptions): Promise<void>;
openDevTools(options?: INativeHostOptions): Promise<void>;
toggleDevTools(options?: INativeHostOptions): Promise<void>;
// Perf Introspection

View file

@ -5,7 +5,7 @@
import * as fs from 'fs';
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 { promisify } from 'util';
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 { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
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 { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
@ -855,14 +855,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#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);
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> {
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

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 {
const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage };
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) {
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');
}
this.colorSchema.properties[id] = {
description,
oneOf: [
propertySchema,
{ 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 { ILoggerResource, LogLevel } from 'vs/platform/log/common/log';
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 { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
@ -240,8 +239,6 @@ export function useWindowControlsOverlay(configurationService: IConfigurationSer
if (typeof setting === 'boolean') {
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.

View file

@ -28,6 +28,7 @@ import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSe
import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor';
import { coalesce, firstOrDefault } from 'vs/base/common/arrays';
import { ICanonicalUriService } from 'vs/platform/workspace/common/canonicalUri';
import { revive } from 'vs/base/common/marshalling';
@extHostNamedCustomer(MainContext.MainThreadWorkspace)
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@ -146,7 +147,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
const query = this._queryBuilder.file(
includeFolder ? [includeFolder] : workspace.folders,
options
revive(options)
);
return this._searchService.fileSearch(query, token).then(result => {
@ -164,7 +165,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
const workspace = this._contextService.getWorkspace();
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';
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 {
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 {
return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider);
},

View file

@ -304,17 +304,6 @@ const newCommands: ApiCommand[] = [
})(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
new ApiCommand(
'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> {
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> {
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 {
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

View file

@ -584,7 +584,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
}
const parsedInclude = include ? parseSearchExcludeInclude(GlobPattern.from(include)) : undefined;
const excludePatterns = include ? globsToISearchPatternBuilder(options.exclude) : undefined;
const excludePatterns = globsToISearchPatternBuilder(options.exclude);
return {
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> {
const requestId = this._requestIdProvider.getNext();
const isCanceled = false;
let isCanceled = false;
token.onCancellationRequested(_ => {
isCanceled = true;
});
this._activeSearchCallbacks[requestId] = p => {
if (isCanceled) {

View file

@ -138,11 +138,11 @@
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);
}
.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;
}
@ -182,7 +182,7 @@
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;
padding: 0 12px;
}
@ -280,7 +280,7 @@
border-left: 1px solid transparent;
}
/* Window Controls (Minimize, Max/Restore, Close) */
/* Window Controls Container */
.monaco-workbench .part.titlebar .window-controls-container {
display: flex;
flex-grow: 0;
@ -292,7 +292,12 @@
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 {
width: calc(100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px));
height: env(titlebar-area-height, 35px);
@ -311,29 +316,31 @@
order: 1;
}
/* Desktop Windows/Linux Window Controls*/
.monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container.primary {
/* Window Controls Container Desktop: apply zoom friendly size */
.monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container {
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;
}
.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 * {
zoom: calc(1 / var(--zoom-factor, 1));
}
/* Desktop macOS Window Controls */
.monaco-workbench:not(.web).mac .part.titlebar .window-controls-container.primary {
.monaco-workbench:not(.web).mac .part.titlebar .window-controls-container {
width: 70px;
}
.monaco-workbench.fullscreen .part.titlebar .window-controls-container {
display: none;
background-color: transparent;
}
/* Window Control Icons */
.monaco-workbench .part.titlebar .window-controls-container > .window-icon {
display: flex;
@ -342,6 +349,11 @@
height: 100%;
width: 46px;
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 {
@ -376,7 +388,6 @@
z-index: 2500;
-webkit-app-region: no-drag;
height: 100%;
min-width: 28px;
}
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-right > .action-toolbar-container {
@ -455,11 +466,3 @@
border-radius: 16px;
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 { MultiWindowParts, Part } from 'vs/workbench/browser/part';
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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@ -228,7 +228,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
const wcoEnabled = isWeb && isWCOEnabled();
let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30;
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);
@ -249,7 +249,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
//#endregion
protected rootContainer!: HTMLElement;
protected primaryWindowControls: HTMLElement | undefined;
protected windowControlsContainer: HTMLElement | undefined;
protected dragRegion: HTMLElement | undefined;
private title!: HTMLElement;
@ -476,21 +476,49 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
this.createActionToolBarMenus();
}
let primaryControlLocation = isMacintosh ? 'left' : 'right';
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';
}
}
// Window Controls Container
if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {
this.primaryWindowControls = append(primaryControlLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container.primary'));
append(primaryControlLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container.secondary'));
let primaryWindowControlsLocation = isMacintosh ? 'left' : 'right';
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

View file

@ -152,7 +152,7 @@ class ChatHistoryAction extends Action2 {
let lastDate: string | undefined = undefined;
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 ? {
type: 'separator', label: timeAgoStr,
} : 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 { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
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 { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView';
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);
registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup);
registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore);
// Disabled until https://github.com/microsoft/vscode/issues/218646 is fixed
// registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
registerChatActions();
registerChatCopyActions();

View file

@ -516,7 +516,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
onDidChangeCursorPosition();
}
private initAttachedContext(container: HTMLElement) {
private initAttachedContext(container: HTMLElement, isLayout = false) {
const oldHeight = container.offsetHeight;
dom.clearNode(container);
this.attachedContextDisposables.clear();
@ -578,7 +578,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.attachedContextDisposables.add(disp);
});
if (oldHeight !== container.offsetHeight) {
if (oldHeight !== container.offsetHeight && !isLayout) {
this._onDidChangeHeight.fire();
}
}
@ -609,7 +609,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private previousInputEditorDimension: IDimension | undefined;
private _layout(height: number, width: number, allowRecurse = true): void {
this.initAttachedContext(this.attachedContextContainer);
this.initAttachedContext(this.attachedContextContainer, true);
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.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays';
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 { 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 { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
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 { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
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_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane';
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 { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
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 {
static readonly ID = 'workbench.contrib.chatExtensionPointHandler';
@ -198,9 +170,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
constructor(
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
@ILogService private readonly logService: ILogService,
@ILogService private readonly logService: ILogService
) {
this._viewContainer = this.registerViewContainer();
this.registerDefaultParticipantView();
this.handleAndRegisterChatExtensions();
}
@ -239,11 +212,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
continue;
}
const store = new DisposableStore();
if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) {
store.add(this.registerDefaultParticipantView(providerDescriptor));
}
const participantsAndCommandsDisambiguation: {
categoryName: string;
description: string;
@ -260,6 +228,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
}
}
const store = new DisposableStore();
store.add(this._chatAgentService.registerAgent(
providerDescriptor.id,
{
@ -318,15 +287,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
return viewContainer;
}
private hasRegisteredDefaultParticipantView = false;
private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable {
if (this.hasRegisteredDefaultParticipantView) {
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;
private registerDefaultParticipantView(): 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.
const name = 'GitHub Copilot';
const viewDescriptor: IViewDescriptor[] = [{
id: CHAT_VIEW_ID,
containerIcon: this._viewContainer.icon,
@ -336,12 +299,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
canToggleVisibility: false,
canMoveView: true,
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);
return toDisposable(() => {
this.hasRegisteredDefaultParticipantView = false;
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 {
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) {
// Model is initialized, and the default agent disappeared, so show welcome view
this.didUnregisterProvider = true;
this._onDidChangeViewWelcomeState.fire();
}
this._onDidChangeViewWelcomeState.fire();
}));
}
@ -114,6 +115,10 @@ export class ChatViewPane extends ViewPane {
}
override shouldShowWelcome(): boolean {
if (!this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel)) {
return true;
}
const noPersistedSessions = !this.chatService.hasSessions();
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);
}
const diffData = await data.diffData;
if (!diffData) {
return;
}
if (!isEditApplied) {
if (!isEditApplied && diffData) {
const viewModel = this.diffEditor.createViewModel({
original: diffData.original,
modified: diffData.modified
@ -801,6 +797,7 @@ export class CodeCompareBlockPart extends Disposable {
} else {
this.diffEditor.setModel(null);
this._lastDiffEditorViewModel.value = undefined;
this._onDidChangeContentHeight.fire();
}
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 { asJson, IRequestService } from 'vs/platform/request/common/request';
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 { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
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;
private readonly _hasDefaultAgent: IContextKey<boolean>;
private readonly _defaultAgentRegistered: IContextKey<boolean>;
constructor(
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService);
this._defaultAgentRegistered = CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.bindTo(this.contextKeyService);
}
registerAgent(id: string, data: IChatAgentData): IDisposable {
@ -246,6 +248,10 @@ export class ChatAgentService implements IChatAgentService {
throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
}
if (data.isDefault) {
this._defaultAgentRegistered.set(true);
}
const that = this;
const commands = data.slashCommands;
data = {
@ -256,8 +262,13 @@ export class ChatAgentService implements IChatAgentService {
};
const entry = { data };
this._agents.set(id, entry);
this._onDidChangeAgents.fire(undefined);
return toDisposable(() => {
this._agents.delete(id);
if (data.isDefault) {
this._defaultAgentRegistered.set(false);
}
this._onDidChangeAgents.fire(undefined);
});
}
@ -445,7 +456,7 @@ export class ChatAgentService implements IChatAgentService {
const participants = this.getAgents().reduce<IChatParticipantMetadata[]>((acc, a) => {
acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] });
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;
}, []);

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_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_HAS_AGENT = new RawContextKey<boolean>('chatInputHasAgent', false);
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.
*/
export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {
normalizeOldFields(raw);
if (!('version' in raw)) {
return {
version: 3,
@ -636,6 +638,30 @@ export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISe
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 {
const data = obj as IExportableChatData;
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> {
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');
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 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 requests = [...model.getRequests()];
let gotProgress = false;
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) {
// 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
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}`);
// 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 pendingRequest = this._pendingRequests.get(sessionId);
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);
rawResult = agentResult;
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)) {
request = model.addRequest(parsedRequest, { variables: [] }, attempt);
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[] = [];
for (const request of model.getRequests()) {
for (const request of requests) {
if (!request.response) {
continue;
}
@ -791,7 +791,7 @@ export class ChatService extends Disposable implements IChatService {
const promptTextResult = getPromptText(request.message);
const historyRequest: IChatAgentRequest = {
sessionId: model.sessionId,
sessionId: sessionId,
requestId: request.id,
agentId: request.response.agent?.id ?? '',
message: promptTextResult.message,

View file

@ -941,12 +941,6 @@ export class StopReadAloud extends Action2 {
when: ScopedChatSynthesisInProgress,
group: 'navigation',
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
),
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 { IStorageService } from 'vs/platform/storage/common/storage';
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 { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@ -230,4 +230,53 @@ suite('normalizeSerializableChatData', () => {
assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate);
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({
showResolved: this.viewState['showResolved'] !== false,
showUnresolved: this.viewState['showUnresolved'] !== false,
sortBy: this.viewState['sortBy'],
sortBy: this.viewState['sortBy'] ?? CommentsSortOrder.ResourceAscending,
}, this.contextKeyService));
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 { localize } from 'vs/nls';
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 { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
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 {
return this._sortBy.get()!;
return this._sortBy.get() ?? CommentsSortOrder.ResourceAscending;
}
set sortBy(sortBy: CommentsSortOrder) {
if (this._sortBy.get() !== sortBy) {
@ -208,7 +208,7 @@ registerAction2(class extends ViewAction<ICommentsView> {
icon: Codicon.history,
viewId: COMMENTS_VIEW_ID,
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"),
},
menu: {
@ -229,13 +229,13 @@ registerAction2(class extends ViewAction<ICommentsView> {
constructor() {
super({
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"),
icon: Codicon.history,
viewId: COMMENTS_VIEW_ID,
toggled: {
condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.ResourceAscending),
title: localize('sorting by file', "File"),
condition: ContextKeyExpr.equals(CONTEXT_KEY_SORT_BY.key, CommentsSortOrder.ResourceAscending),
title: localize('sorting by position in file', "Position in File"),
},
menu: {
id: commentSortSubmenu,

View file

@ -12,20 +12,24 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
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 { Constants } from 'vs/base/common/uint';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import 'vs/css!./media/callStackWidget';
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 { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
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 { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { Location } from 'vs/editor/common/languages';
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 { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
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 { ResourceLabel } from 'vs/workbench/browser/labels';
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 {
@ -97,6 +101,9 @@ class WrappedCustomStackFrame implements IFrameLikeItem {
constructor(public readonly original: CustomStackFrame) { }
}
const isFrameLike = (item: unknown): item is IFrameLikeItem =>
item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame;
type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame;
const WIDGET_CLASS_NAME = 'multiCallStackWidget';
@ -137,6 +144,7 @@ export class CallStackWidget extends Disposable {
multipleSelectionSupport: false,
mouseSupport: false,
keyboardSupport: false,
setRowLineHeight: false,
accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider),
}
) as WorkbenchList<ListItem>);
@ -157,6 +165,17 @@ export class CallStackWidget extends Disposable {
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> {
if (!this.cts) {
return;
@ -356,9 +375,9 @@ abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateDat
collapse.element.ariaExpanded = String(!collapsed);
elements.root.classList.toggle('collapsed', collapsed);
}));
elementStore.add(collapse.onDidClick(() => {
item.collapsed.set(!item.collapsed.get(), undefined);
}));
const toggleCollapse = () => 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 {
@ -382,26 +401,33 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
private readonly containingEditor: ICodeEditor | undefined,
private readonly onLayout: Event<void>,
@ITextModelService private readonly modelService: ITextModelService,
@ICodeEditorService private readonly editorService: ICodeEditorService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super(instantiationService);
}
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
? this.instantiationService.createInstance(
EmbeddedCodeEditorWidget,
data.elements.editor,
editorOptions,
{ isSimpleWidget: true },
{ isSimpleWidget: true, contributions },
this.containingEditor,
)
: this.instantiationService.createInstance(
CodeEditorWidget,
data.elements.editor,
editorOptions,
{ isSimpleWidget: true },
{ isSimpleWidget: true, contributions },
);
data.templateStore.add(editor);
@ -423,20 +449,6 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
const uri = item.source!;
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();
elementStore.add(toDisposable(() => cts.dispose(true)));
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 {
constructor() {
super({

View file

@ -24,6 +24,10 @@
&[role="link"] {
cursor: pointer;
}
.monaco-icon-label::before {
height: auto;
}
}
&.collapsed {
@ -39,6 +43,7 @@
.collapse-button {
width: 16px;
min-height: 1px; /* show even if empty */
line-height: 0;
a {
cursor: pointer;
@ -56,6 +61,11 @@
.multiCallStackWidget {
.multiCallStackFrameContainer {
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 {
server?: IExtensionManagementServer;
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 { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
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 { Action2, IAction2Options } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
@ -287,7 +287,7 @@ export class DiscardHunkAction extends AbstractInlineChatAction {
constructor() {
super({
id: 'inlineChat.discardHunkChange',
id: ACTION_DISCARD_CHANGES,
title: localize('discard', 'Discard'),
icon: Codicon.chromeClose,
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 { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
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 { 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';
@ -622,6 +622,7 @@ export class InlineChatController implements IEditorContribution {
return;
}
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);
log('move was requested', e.target, e.range);
@ -636,13 +637,13 @@ export class InlineChatController implements IEditorContribution {
}
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');
return;
}
if (!this._session) {
log('controller does not have a session');
if (this._inlineChatSessionService.getSession(newEditor, e.target)) {
log('new editor ALREADY has a session');
return;
}
@ -741,7 +742,7 @@ export class InlineChatController implements IEditorContribution {
await responsePromise.p;
await progressiveEditsQueue.whenIdle();
if (response.isCanceled) {
if (response.result?.errorDetails) {
await this._session.undoChangesUntil(response.requestId);
}
@ -758,7 +759,6 @@ export class InlineChatController implements IEditorContribution {
if (response.result?.errorDetails) {
//
await this._session.undoChangesUntil(response.requestId);
} else if (response.response.value.length === 0) {
// empty -> show message
@ -990,8 +990,21 @@ export class InlineChatController implements IEditorContribution {
// ---- controller API
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'] });
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() {

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.)
this._session = session;
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._sessionService.releaseSession(session);
this._ctxHasStashedSession.reset();
@ -360,27 +360,30 @@ export class HunkData {
// mirror textModelN changes to textModel0 execept for those that
// overlap with a hunk
type HunkRangePair = { rangeN: Range; range0: Range };
type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void };
const hunkRanges: HunkRangePair[] = [];
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
for (let i = 1; i < textModelNDecorations.length; i++) {
const rangeN = this._textModelN.getDecorationRange(textModelNDecorations[i]);
const range0 = this._textModel0.getDecorationRange(textModel0Decorations[i]);
for (let i = 1; i < entry.textModelNDecorations.length; i++) {
const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]);
const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
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
for (let i = 1; i < textModel0Decorations.length; i++) {
const range = this._textModel0.getDecorationRange(textModel0Decorations[i]);
for (let i = 1; i < entry.textModel0Decorations.length; i++) {
const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
if (range) {
ranges0.push(range);
}
@ -399,16 +402,20 @@ export class HunkData {
let pendingChangesLen = 0;
for (const { rangeN, range0 } of hunkRanges) {
if (rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) {
for (const entry of hunkRanges) {
if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) {
// 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
// actual insertions/deletions. Therefore we need to take the length of the original
// range into account.
pendingChangesLen += this._textModelN.getValueLengthInRange(rangeN);
pendingChangesLen -= this._textModel0.getValueLengthInRange(range0);
pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN);
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;
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');
if (!diff || diff.changes.length === 0) {
// return new HunkData([], session);
return;
}
let mergedChanges: DetailedLineRangeMapping[] = [];
// merge changes neighboring changes
const mergedChanges = [diff.changes[0]];
for (let i = 1; i < diff.changes.length; i++) {
const lastChange = mergedChanges[mergedChanges.length - 1];
const thisChange = diff.changes[i];
if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) {
mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping(
lastChange.original.join(thisChange.original),
lastChange.modified.join(thisChange.modified),
(lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? [])
);
} else {
mergedChanges.push(thisChange);
if (diff && diff.changes.length > 0) {
// merge changes neighboring changes
mergedChanges = [diff.changes[0]];
for (let i = 1; i < diff.changes.length; i++) {
const lastChange = mergedChanges[mergedChanges.length - 1];
const thisChange = diff.changes[i];
if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) {
mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping(
lastChange.original.join(thisChange.original),
lastChange.modified.join(thisChange.modified),
(lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? [])
);
} 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 { Emitter, Event } from 'vs/base/common/event';
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 { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
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 { HunkInformation, Session, HunkState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
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 { IModelService } from 'vs/editor/common/services/model';
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 { EditorOption } from 'vs/editor/common/config/editorOptions';
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 {
start(): void;
@ -216,8 +219,10 @@ type HunkDisplayData = {
decorationIds: string[];
viewZoneId: string | undefined;
viewZone: IViewZone;
diffViewZoneId: string | undefined;
diffViewZone: IViewZone;
lensActionsViewZoneIds?: string[];
distance: number;
position: Position;
@ -257,6 +262,7 @@ export class LiveStrategy extends EditModeStrategy {
private readonly _ctxCurrentChangeShowsDiff: IContextKey<boolean>;
private readonly _progressiveEditingDecorations: IEditorDecorationsCollection;
private readonly _lensActionsFactory: ConflictActionsFactory;
private _editCount: number = 0;
constructor(
@ -268,6 +274,8 @@ export class LiveStrategy extends EditModeStrategy {
@IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IConfigurationService private readonly _configService: IConfigurationService,
@IMenuService private readonly _menuService: IMenuService,
@IContextKeyService private readonly _contextService: IContextKeyService,
@ITextFileService textFileService: ITextFileService,
@IInstantiationService instaService: IInstantiationService
) {
@ -276,6 +284,7 @@ export class LiveStrategy extends EditModeStrategy {
this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService);
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,
heightInLines: result.heightInLines,
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 scrollState = StableEditorScrollState.capture(this._editor);
changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => {
assertType(data);
if (!data.viewZoneId) {
if (!data.diffViewZoneId) {
const [hunkRange] = hunkData.getRangesN();
viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1;
data.viewZoneId = viewZoneAccessor.addZone(viewZoneData);
data.diffViewZoneId = viewZoneAccessor.addZone(viewZoneData);
overlay?.updateExtraTop(result.heightInLines);
} else {
viewZoneAccessor.removeZone(data.viewZoneId!);
viewZoneAccessor.removeZone(data.diffViewZoneId!);
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);
};
const overlay = this._showOverlayToolbar
const overlay = this._showOverlayToolbar && false
? this._instaService.createInstance(InlineChangeOverlay, this._editor, hunkData)
: 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 = () => {
changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => {
assertType(data);
for (const decorationId of data.decorationIds) {
decorationsAccessor.removeDecoration(decorationId);
}
if (data.viewZoneId) {
viewZoneAccessor.removeZone(data.viewZoneId);
if (data.diffViewZoneId) {
viewZoneAccessor.removeZone(data.diffViewZoneId);
}
data.decorationIds = [];
data.viewZoneId = undefined;
data.diffViewZoneId = undefined;
data.lensActionsViewZoneIds?.forEach(viewZoneAccessor.removeZone);
data.lensActionsViewZoneIds = undefined;
});
lensActions?.dispose();
overlay?.dispose();
};
@ -548,8 +607,9 @@ export class LiveStrategy extends EditModeStrategy {
data = {
hunk: hunkData,
decorationIds,
viewZoneId: '',
viewZone: viewZoneData,
diffViewZoneId: '',
diffViewZone: viewZoneData,
lensActionsViewZoneIds,
distance: myDistance,
position: hunkRanges[0].getStartPosition().delta(-1),
acceptHunk,

View file

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

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener, Dimension } from 'vs/base/browser/dom';
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 { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';
@ -29,6 +29,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: EditorBasedInlineChatWidget;
private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor));
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
private _dimension?: Dimension;
@ -165,6 +166,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
this.widget.focus();
revealZone();
this._scrollUp.enable();
}
override updatePositionAndHeight(position: Position): void {
@ -186,14 +188,15 @@ export class InlineChatZoneWidget extends ZoneWidget {
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)
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);
};
});
}
return () => {
return this._scrollUp.runIgnored(() => {
scrollState.restore(this.editor);
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.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
}
};
});
}
protected override revealRange(range: Range, isLastLine: boolean): void {
@ -229,6 +232,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
override hide(): void {
const scrollState = StableEditorBottomScrollState.capture(this.editor);
this._scrollUp.disable();
this._ctxCursorPosition.reset();
this.widget.reset();
this.widget.chatWidget.setVisible(false);
@ -237,3 +241,54 @@ export class InlineChatZoneWidget extends ZoneWidget {
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 {
padding: 4px 6px 0 6px;
padding: 2px 6px 0 6px;
}
.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar {
@ -32,6 +32,12 @@
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 {
padding: 4px 0 0 0;
}
@ -70,7 +76,15 @@
display: flex;
justify-content: space-between;
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 {
@ -92,7 +106,8 @@
.monaco-workbench .inline-chat .status .label.status {
margin-left: auto;
padding: 0 6px;
padding-right: 6px;
padding-left: 6px;
}
.monaco-workbench .inline-chat .status .label.hidden,
@ -156,6 +171,16 @@
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-button {

View file

@ -116,6 +116,7 @@ export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey<InlineChatRespons
// --- (selected) action identifier
export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges';
export const ACTION_DISCARD_CHANGES = 'inlineChat.discardHunkChange';
export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate';
export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat';
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');
@ -819,7 +819,7 @@ suite('InteractiveChatController', function () {
chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);
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 { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService';
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 () {
@ -487,4 +489,90 @@ suite('InlineChatSession', function () {
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}}`;
}
this._styleElement.textContent = newStyle;
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
this._editor.getContainerDomNode().style?.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
this._editor.getContainerDomNode().style?.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
}
private _getLayoutInfo() {

View file

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

View file

@ -1074,7 +1074,7 @@ export class DeletedElement extends SingleSideDiffElement {
layout(state: IDiffElementLayoutState) {
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._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
@ -1254,7 +1254,7 @@ export class InsertElement extends SingleSideDiffElement {
layout(state: IDiffElementLayoutState) {
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._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
@ -1644,7 +1644,7 @@ export class ModifiedElement extends AbstractElementRenderer {
{
updateInfoRendering: () => renderSourceEditor(),
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,
updateFoldingState: (cell, state) => cell.cellFoldingState = state,
@ -1660,7 +1660,7 @@ export class ModifiedElement extends AbstractElementRenderer {
const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer);
this._register(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 ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
@ -1675,7 +1675,7 @@ export class ModifiedElement extends AbstractElementRenderer {
const refreshToolbar = () => {
const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
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);
if (hasChanges) {

View file

@ -222,7 +222,7 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB
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.outputFoldingState = PropertyFoldingState.Collapsed;

View file

@ -441,16 +441,12 @@ function createDiffViewModels(instantiationService: IInstantiationService, confi
);
}
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(
model.modified.notebook,
model.original.notebook,
originalCell,
modifiedCell,
type,
eventDispatcher,
originalModel.cells[diff.originalCellIndex],
modifiedModel.cells[diff.modifiedCellIndex],
'unchanged', eventDispatcher,
initData,
notebookService
);

View file

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

View file

@ -314,7 +314,7 @@ class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemViewModelTreeEl
const historyItemLabels = (historyItem.labels ?? [])
.filter(l => labels.includes(l.title));
if (historyItemLabels) {
if (historyItemLabels.length > 0) {
const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal);
const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote);
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> {
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), {
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(
query,
token
).then((ret) => {
if (!ret.limitHit) {
throw Error('File not found');
}
return !!ret.limitHit;
});
});
return Promise.any(promises).then(() => true).catch(() => false);
return Promise.any(promises);
}
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_status="$?"
builtin local cmd
__vsc_restore_exit_code "${__vsc_status}"
# Evaluate the original PROMPT_COMMAND similarly to how bash would normally
# See https://unix.stackexchange.com/a/672843 for technique
builtin local cmd
for cmd in "${__vsc_original_prompt_command[@]}"; do
eval "${cmd:-}"
done

View file

@ -172,9 +172,9 @@ function Set-MappedKeyHandler {
function Get-KeywordCompletionResult(
$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 {
@ -360,7 +360,7 @@ function Send-Completions {
# completions are consistent regardless of where it was requested
elseif ($lastWord -match '[/\\]') {
$lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\'))
if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) {
if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) {
$newCursorIndex = $lastSlashIndex + 1
$completionPrefix = $completionPrefix.Substring(0, $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 })) {
# Add `../ relative to the top completion
$firstCompletion = $completions.CompletionMatches[0]
if ($firstCompletion.CompletionText.StartsWith('../')) {
if ($completionPrefix -match '(\.\.\/)+') {
$parentDir = "$($matches[0])../"
if ($firstCompletion.CompletionText.StartsWith("..$([System.IO.Path]::DirectorySeparatorChar)")) {
if ($completionPrefix -match "(\.\.\$([System.IO.Path]::DirectorySeparatorChar))+") {
$parentDir = "$($matches[0])..$([System.IO.Path]::DirectorySeparatorChar)"
$currentPath = Split-Path -Parent $firstCompletion.ToolTip
try {
$parentDirPath = Split-Path -Parent $currentPath
@ -430,7 +430,7 @@ function Send-Completions {
# completions are consistent regardless of where it was requested
if ($completionPrefix -match '[/\\]') {
$lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\'))
if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) {
if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) {
$newCursorIndex = $lastSlashIndex + 1
$completionPrefix = $completionPrefix.Substring(0, $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> {
// 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 };
}

View file

@ -9,6 +9,7 @@ import { AutoOpenBarrier } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isWindows } from 'vs/base/common/platform';
import { localize2 } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
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
// received. This fixing some problems in PowerShell, particularly enter not executing
// when typing quickly and some characters being printed twice.
let barrier: AutoOpenBarrier | undefined;
this.add(addon.onDidRequestCompletions(() => {
barrier = new AutoOpenBarrier(2000);
this._instance.pauseInputEvents(barrier);
}));
this.add(addon.onDidReceiveCompletions(() => {
barrier?.open();
barrier = undefined;
}));
// when typing quickly and some characters being printed twice. On Windows this isn't
// needed because inputs are _not_ echoed when not handled immediately.
// TODO: This should be based on the OS of the pty host, not the client
if (!isWindows) {
let barrier: AutoOpenBarrier | undefined;
this.add(addon.onDidRequestCompletions(() => {
barrier = new AutoOpenBarrier(2000);
this._instance.pauseInputEvents(barrier);
}));
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[]) {
this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame(
frame.label,

View file

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

View file

@ -838,22 +838,21 @@ class TreeActionsProvider {
}
if (element instanceof TestMessageElement) {
id = MenuId.TestMessageContext;
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);
primary.push(new Action(
'testing.outputPeek.goToFile',
localize('testing.goToFile', "Go to Source"),
'testing.outputPeek.goToTest',
localize('testing.goToTest', "Go to Test"),
ThemeIcon.asClassName(Codicon.goToFile),
undefined,
() => 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) {
primary.push(new Action(
'testing.outputPeek.goToError',
localize('testing.goToError', "Go to Source"),
localize('testing.goToError', "Go to Error"),
ThemeIcon.asClassName(Codicon.goToFile),
undefined,
() => 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) {
if (!(subject instanceof MessageSubject)) {
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 { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
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 { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
@ -136,6 +136,7 @@ registerAction2(GoToPreviousMessageAction);
registerAction2(GoToNextMessageAction);
registerAction2(CloseTestPeek);
registerAction2(ToggleTestingPeekHistory);
registerAction2(CollapsePeekStack);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually);

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
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 { equals } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async';
@ -456,11 +457,24 @@ export class TestingDecorations extends Disposable implements IEditorContributio
decorations.syncDecorations(this._currentUri);
}
}));
this._register(this.editor.onKeyDown(e => {
if (e.keyCode === KeyCode.Alt && this._currentUri) {
decorations.updateDecorationsAlternateAction(this._currentUri!, true);
const win = dom.getWindow(editor.getDomNode());
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 => {
if (e.keyCode === KeyCode.Alt && this._currentUri) {
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 { Lazy } from 'vs/base/common/lazy';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { observableValue } from 'vs/base/common/observable';
import { count } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
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 { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
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 { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
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 { IViewDescriptorService } from 'vs/workbench/common/views';
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 { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme';
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();
}
/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.peek.value?.collapseStack();
}
/**
* Shows the next message in the peek, if possible.
*/
@ -645,10 +654,14 @@ class TestResultsPeek extends PeekViewWidget {
private static lastHeightInLines?: number;
private readonly visibilityChange = this._disposables.add(new Emitter<boolean>());
private readonly _current = observableValue<InspectSubject | undefined>('testPeekCurrent', undefined);
private content!: TestResultsViewContent;
private scopedContextKeyService!: IContextKeyService;
private dimension?: dom.Dimension;
public current?: InspectSubject;
public get current() {
return this._current.get();
}
constructor(
editor: ICodeEditor,
@ -702,7 +715,14 @@ class TestResultsPeek extends PeekViewWidget {
protected override _fillHead(container: HTMLElement): void {
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!;
this._disposables.add(menu.onDidChange(() => {
actions.length = 0;
@ -732,7 +752,7 @@ class TestResultsPeek extends PeekViewWidget {
*/
public setModel(subject: InspectSubject): Promise<void> {
if (subject instanceof TaskSubject || subject instanceof TestOutputSubject) {
this.current = subject;
this._current.set(subject, undefined);
return this.showInPlace(subject);
}
@ -743,14 +763,14 @@ class TestResultsPeek extends PeekViewWidget {
return Promise.resolve();
}
this.current = subject;
this._current.set(subject, undefined);
if (!revealLocation) {
return this.showInPlace(subject);
}
// If there is a stack we want to display, ensure the default size is large-ish
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)
);
@ -760,6 +780,13 @@ class TestResultsPeek extends PeekViewWidget {
return this.showInPlace(subject);
}
/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.content.collapseStack();
}
private getVisibleEditorLines() {
// 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.
@ -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 {
public static readonly ID = 'testing.openMessageInEditor';
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 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 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> } = {
[TestRunProfileBitset.Run]: hasRunnableTests,

View file

@ -92,6 +92,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
private profileWidget: ProfileWidget | undefined;
private model: UserDataProfilesEditorModel | undefined;
private templates: readonly IProfileTemplateInfo[] = [];
constructor(
group: IEditorGroup,
@ -207,7 +208,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
actions: {
getActions: () => {
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 Separator());
}
@ -225,15 +226,13 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi
}
private getCreateFromTemplateActions(): IAction[] {
return this.model
? this.model.templates.map(template =>
new Action(
`template:${template.url}`,
template.name,
undefined,
true,
() => this.createNewProfile(URI.parse(template.url))))
: [];
return this.templates.map(template =>
new Action(
`template:${template.url}`,
template.name,
undefined,
true,
() => this.createNewProfile(URI.parse(template.url))));
}
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> {
await super.setInput(input, options, context, token);
this.model = await input.resolve();
if (this.profileWidget) {
this.profileWidget.templates = this.model.templates;
}
this.model.getTemplates().then(templates => {
this.templates = templates;
if (this.profileWidget) {
this.profileWidget.templates = templates;
}
});
this.updateProfilesList();
this._register(this.model.onDidChange(element =>
this.updateProfilesList(element)));
@ -710,7 +712,6 @@ class ProfileTreeDataSource implements IAsyncDataSource<AbstractUserDataProfileE
children.push({ element: 'name', root: element });
children.push({ element: 'icon', root: element });
}
children.push({ element: 'useForCurrent', root: element });
children.push({ element: 'useAsDefault', 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>());
readonly onDidChange = this._onDidChange.event;
private _templates: IProfileTemplateInfo[] | undefined;
get templates(): readonly IProfileTemplateInfo[] { return this._templates ?? []; }
private templates: Promise<readonly IProfileTemplateInfo[]> | undefined;
constructor(
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@ -761,9 +760,11 @@ export class UserDataProfilesEditorModel extends EditorModel {
}
}
override async resolve(): Promise<void> {
await super.resolve();
this._templates = await this.userDataProfileManagementService.getBuiltinProfileTemplates();
getTemplates(): Promise<readonly IProfileTemplateInfo[]> {
if (!this.templates) {
this.templates = this.userDataProfileManagementService.getBuiltinProfileTemplates();
}
return this.templates;
}
private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] {
@ -771,7 +772,7 @@ export class UserDataProfilesEditorModel extends EditorModel {
const activateAction = disposables.add(new Action(
'userDataProfile.activate',
localize('active', "Use for Current Window"),
localize('active', "Use this Profile for Current Window"),
ThemeIcon.asClassName(Codicon.check),
true,
() => this.userDataProfileManagementService.switchProfile(profileElement.profile)
@ -808,25 +809,16 @@ export class UserDataProfilesEditorModel extends EditorModel {
() => 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[] = [];
primaryActions.push(activateAction);
primaryActions.push(newWindowAction);
if (!profile.isDefault) {
primaryActions.push(deleteAction);
}
const secondaryActions: IAction[] = [];
secondaryActions.push(activateAction);
secondaryActions.push(useAsNewWindowProfileAction);
secondaryActions.push(new Separator());
secondaryActions.push(copyFromProfileAction);
secondaryActions.push(exportAction);
if (!profile.isDefault) {
secondaryActions.push(new Separator());
secondaryActions.push(deleteAction);
}
const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement,
profile,
@ -834,16 +826,9 @@ export class UserDataProfilesEditorModel extends EditorModel {
[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(() =>
activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id));
useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile;
disposables.add(profileElement.onDidChange(e => {
if (e.newWindowProfile) {
useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile;
}
}));
activateAction.enabled = this.userDataProfileService.currentProfile.id !== profileElement.profile.id));
return [profileElement, disposables];
}

View file

@ -226,6 +226,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
'onSettingChanged:workbench.colorTheme',
'onCommand:workbench.action.selectTheme'
],
when: '!accessibilityModeEnabled',
media: { type: 'markdown', path: 'theme_picker', }
},
{
@ -399,7 +400,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
isFeatured: true,
icon: setupIcon,
when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key,
next: 'SetupScreenReaderExtended',
next: 'Setup',
content: {
type: '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',
isFeatured: false,

View file

@ -28,7 +28,6 @@ import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
import { ModifierKeyEmitter } from 'vs/base/browser/dom';
import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from 'vs/workbench/common/configuration';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sandbox/window';
import product from 'vs/platform/product/common/product';
// Actions
(function registerActions(): void {
@ -240,7 +239,7 @@ import product from 'vs/platform/product/common/product';
'type': 'boolean',
'included': isLinux,
'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': {
'type': 'string',

View file

@ -156,17 +156,17 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
})));
}
// Window Controls (Native Windows/Linux)
if (!isMacintosh && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.primaryWindowControls) {
// Window Controls (Native Linux when WCO is disabled)
if (isLinux && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.windowControlsContainer) {
// 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.nativeHostService.minimizeWindow({ targetWindowId });
}));
// 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 () => {
const maximized = await this.nativeHostService.isMaximized({ targetWindowId });
if (maximized) {
@ -177,7 +177,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
}));
// 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.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;
protected readonly extensionsManager: ExtensionsManager;
private readonly storageManger: StorageManager;
private readonly storageManager: StorageManager;
constructor(
@IStorageService storageService: IStorageService,
@ -63,7 +63,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
@IInstantiationService instantiationService: IInstantiationService,
) {
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)));
let isDisposed = false;
@ -610,11 +610,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
if (!this.hasWorkspace) {
return [];
}
return this.storageManger.get(storageId, StorageScope.WORKSPACE);
return this.storageManager.get(storageId, StorageScope.WORKSPACE);
}
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> {

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 { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes';
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 {
base: URI;
@ -53,7 +55,7 @@ class FileSearchEngine {
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.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || undefined;
@ -116,10 +118,11 @@ class FileSearchEngine {
private async doSearch(fqs: IFolderQuery<URI>[], onResult: (match: IInternalFileMatch) => void): Promise<IFileSearchProviderStats | null> {
const cancellation = new CancellationTokenSource();
const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq));
const session = this.provider instanceof OldFileSearchProviderConverter ? this.sessionLifecycle?.tokenSource.token : this.sessionLifecycle?.obj;
const options: FileSearchProviderOptions = {
folderOptions,
maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS,
session: this.sessionToken
session
};
@ -301,11 +304,30 @@ interface IInternalSearchComplete {
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 {
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> {
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
@ -333,17 +355,19 @@ export class FileSearchManager {
}
clearCache(cacheKey: string): void {
// cancel the token
this.sessions.get(cacheKey)?.dispose();
// with no reference to this, it will be removed from WeakMaps
this.sessions.delete(cacheKey);
}
private getSessionTokenSource(cacheKey: string | undefined): unknown {
private getSessionTokenSource(cacheKey: string | undefined): SessionLifecycle | undefined {
if (!cacheKey) {
return undefined;
}
if (!this.sessions.has(cacheKey)) {
this.sessions.set(cacheKey, new Object());
this.sessions.set(cacheKey, new SessionLifecycle());
}
return this.sessions.get(cacheKey);

View file

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

View file

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

View file

@ -271,6 +271,9 @@ export class SearchService extends Disposable implements ISearchService {
return [];
}
await Promise.all([...fqs.keys()].map(async scheme => {
if (query.onlyFileScheme && scheme !== Schemas.file) {
return;
}
const schemeFQs = fqs.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 quit(): 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 resolveProxy(url: string): Promise<string | 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.
*--------------------------------------------------------------------------------------------*/
//#region --- void
import 'vs/workbench/contrib/void/browser/void.contribution'
//#endregion
//#region --- editor/workbench core
import 'vs/editor/editor.all';
@ -15,7 +10,6 @@ import 'vs/editor/editor.all';
import 'vs/workbench/api/browser/extensionHost.contribution';
import 'vs/workbench/browser/workbench.contribution';
//#endregion