Merge branch 'main' into joh/chatWidget

This commit is contained in:
Johannes
2024-03-21 12:10:52 +01:00
89 changed files with 2082 additions and 629 deletions
+16
View File
@@ -662,12 +662,28 @@ pub struct TunnelServeArgs {
/// Requests that extensions be preloaded and installed on connecting servers.
#[clap(long)]
pub install_extension: Vec<String>,
/// Specifies the directory that server data is kept in.
#[clap(long)]
pub server_data_dir: Option<String>,
/// Set the root path for extensions.
#[clap(long)]
pub extensions_dir: Option<String>,
}
impl TunnelServeArgs {
pub fn apply_to_server_args(&self, csa: &mut CodeServerArgs) {
csa.install_extensions
.extend_from_slice(&self.install_extension);
if let Some(d) = &self.server_data_dir {
csa.server_data_dir = Some(d.clone());
}
if let Some(d) = &self.extensions_dir {
csa.extensions_dir = Some(d.clone());
}
}
}
+8
View File
@@ -57,6 +57,8 @@ pub struct CodeServerArgs {
pub log: Option<log::Level>,
pub accept_server_license_terms: bool,
pub verbose: bool,
pub server_data_dir: Option<String>,
pub extensions_dir: Option<String>,
// extension management
pub install_extensions: Vec<String>,
pub uninstall_extensions: Vec<String>,
@@ -144,6 +146,12 @@ impl CodeServerArgs {
args.push(format!("--category={}", i));
}
}
if let Some(d) = &self.server_data_dir {
args.push(format!("--server-data-dir={}", d));
}
if let Some(d) = &self.extensions_dir {
args.push(format!("--extensions-dir={}", d));
}
if self.start_server {
args.push(String::from("--start-server"));
}
@@ -38,14 +38,14 @@
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.",
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.",
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
"configuration.markdown.editor.drop.always": "Always insert Markdown links.",
"configuration.markdown.editor.drop.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.drop.never": "Never create Markdown links.",
"configuration.markdown.editor.drop.enabled.always": "Always insert Markdown links.",
"configuration.markdown.editor.drop.enabled.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.drop.enabled.never": "Never create Markdown links.",
"configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created",
"configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
"configuration.markdown.editor.filePaste.always": "Always insert Markdown links.",
"configuration.markdown.editor.filePaste.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.filePaste.never": "Never create Markdown links.",
"configuration.markdown.editor.filePaste.enabled.always": "Always insert Markdown links.",
"configuration.markdown.editor.filePaste.enabled.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.filePaste.enabled.never": "Never create Markdown links.",
"configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.",
"configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",
+1
View File
@@ -65,6 +65,7 @@
"contributes": {
"chatParticipants": [
{
"id": "api-test.participant",
"name": "participant",
"description": "test",
"isDefault": true,
@@ -30,7 +30,7 @@ suite('chat', () => {
function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> {
const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>();
disposables.push();
disposables.push(emitter);
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
@@ -40,7 +40,7 @@ suite('chat', () => {
},
}));
const participant = chat.createChatParticipant('participant', (request, context, _progress, _token) => {
const participant = chat.createChatParticipant('api-test.participant', (request, context, _progress, _token) => {
emitter.fire({ request, context });
return null;
});
@@ -49,12 +49,12 @@ suite('chat', () => {
return emitter.event;
}
test('participant and slash command', async () => {
test('participant and slash command history', async () => {
const onRequest = setupParticipant();
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
let i = 0;
onRequest(request => {
disposables.push(onRequest(request => {
if (i === 0) {
assert.deepStrictEqual(request.request.command, 'hello');
assert.strictEqual(request.request.prompt, 'friend');
@@ -62,10 +62,10 @@ suite('chat', () => {
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
} else {
assert.strictEqual(request.context.history.length, 1);
assert.strictEqual(request.context.history[0].participant.name, 'participant');
assert.strictEqual(request.context.history[0].participant, 'api-test.participant');
assert.strictEqual(request.context.history[0].command, 'hello');
}
});
}));
});
test('participant and variable', async () => {
@@ -93,7 +93,7 @@ suite('chat', () => {
}));
const deferred = new DeferredPromise<ChatResult>();
const participant = chat.createChatParticipant('participant', (_request, _context, _progress, _token) => {
const participant = chat.createChatParticipant('api-test.participant', (_request, _context, _progress, _token) => {
return { metadata: { key: 'value' } };
});
participant.isDefault = true;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.88.0",
"distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd",
"distro": "7734ec27c8ec09ddc68bc3618e17d9a8b40fbfd9",
"author": {
"name": "Microsoft Corporation"
},
Binary file not shown.
@@ -312,6 +312,18 @@ export class InputBox extends Widget {
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
}
public getSelection(): IRange | null {
const selectionStart = this.input.selectionStart;
if (selectionStart === null) {
return null;
}
const selectionEnd = this.input.selectionEnd ?? selectionStart;
return {
start: selectionStart,
end: selectionEnd,
};
}
public enable(): void {
this.input.removeAttribute('disabled');
}
+1 -1
View File
@@ -237,7 +237,7 @@ export class Checkbox extends Widget {
constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) {
super();
this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles });
this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }));
this.domNode = this.checkbox.domNode;
+8
View File
@@ -525,7 +525,11 @@ export const codiconsLibrary = {
blank: register('blank', 0xec03),
heartFilled: register('heart-filled', 0xec04),
map: register('map', 0xec05),
mapHorizontal: register('map-horizontal', 0xec05),
foldHorizontal: register('fold-horizontal', 0xec05),
mapFilled: register('map-filled', 0xec06),
mapHorizontalFilled: register('map-horizontal-filled', 0xec06),
foldHorizontalFilled: register('fold-horizontal-filled', 0xec06),
circleSmall: register('circle-small', 0xec07),
bellSlash: register('bell-slash', 0xec08),
bellSlashDot: register('bell-slash-dot', 0xec09),
@@ -567,4 +571,8 @@ export const codiconsLibrary = {
runAllCoverage: register('run-all-coverage', 0xec2d),
coverage: register('coverage', 0xec2e),
githubProject: register('github-project', 0xec2f),
mapVertical: register('map-vertical', 0xec30),
foldVertical: register('fold-vertical', 0xec30),
mapVerticalFilled: register('map-vertical-filled', 0xec31),
foldVerticalFilled: register('fold-vertical-filled', 0xec31),
} as const;
+78
View File
@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { constants as FSConstants } from 'fs';
import { access } from 'fs/promises';
import { join } from 'vs/base/common/path';
import { env } from 'vs/base/common/process';
const XDG_SESSION_TYPE = 'XDG_SESSION_TYPE';
const WAYLAND_DISPLAY = 'WAYLAND_DISPLAY';
const XDG_RUNTIME_DIR = 'XDG_RUNTIME_DIR';
const enum DisplayProtocolType {
Wayland = 'wayland',
XWayland = 'xwayland',
X11 = 'x11',
Unknown = 'unknown'
}
export async function getDisplayProtocol(errorLogger: (error: any) => void): Promise<DisplayProtocolType> {
const xdgSessionType = env[XDG_SESSION_TYPE];
if (xdgSessionType) {
// If XDG_SESSION_TYPE is set, return its value if it's either 'wayland' or 'x11'.
// We assume that any value other than 'wayland' or 'x11' is an error or unexpected,
// hence 'unknown' is returned.
return xdgSessionType === DisplayProtocolType.Wayland || xdgSessionType === DisplayProtocolType.X11 ? xdgSessionType : DisplayProtocolType.Unknown;
} else {
const waylandDisplay = env[WAYLAND_DISPLAY];
if (!waylandDisplay) {
// If WAYLAND_DISPLAY is empty, then the session is x11.
return DisplayProtocolType.X11;
} else {
const xdgRuntimeDir = env[XDG_RUNTIME_DIR];
if (!xdgRuntimeDir) {
// If XDG_RUNTIME_DIR is empty, then the session can only be guessed.
return DisplayProtocolType.Unknown;
} else {
// Check for the presence of the file $XDG_RUNTIME_DIR/wayland-0.
const waylandServerPipe = join(xdgRuntimeDir, 'wayland-0');
try {
await access(waylandServerPipe, FSConstants.R_OK);
// If the file exists, then the session is wayland.
return DisplayProtocolType.Wayland;
} catch (err) {
// If the file does not exist or an error occurs, we guess 'unknown'
// since WAYLAND_DISPLAY was set but no wayland-0 pipe could be confirmed.
errorLogger(err);
return DisplayProtocolType.Unknown;
}
}
}
}
}
export function getCodeDisplayProtocol(displayProtocol: DisplayProtocolType, ozonePlatform: string | undefined): DisplayProtocolType {
if (!ozonePlatform) {
return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11;
} else {
switch (ozonePlatform) {
case 'auto':
return displayProtocol;
case 'x11':
return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11;
case 'wayland':
return DisplayProtocolType.Wayland;
default:
return DisplayProtocolType.Unknown;
}
}
}
@@ -118,6 +118,7 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme
import { SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess';
import { getOSReleaseInfo } from 'vs/base/node/osReleaseInfo';
import { getDesktopEnvironment } from 'vs/base/common/desktopEnvironmentInfo';
import { getCodeDisplayProtocol, getDisplayProtocol } from 'vs/base/node/osDisplayProtocolInfo';
class SharedProcessMain extends Disposable implements IClientConnectionFilter {
@@ -465,14 +466,20 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
private async reportClientOSInfo(telemetryService: ITelemetryService, logService: ILogService): Promise<void> {
if (isLinux) {
const releaseInfo = await getOSReleaseInfo(logService.error.bind(logService));
const [releaseInfo, displayProtocol] = await Promise.all([
getOSReleaseInfo(logService.error.bind(logService)),
getDisplayProtocol(logService.error.bind(logService))
]);
const desktopEnvironment = getDesktopEnvironment();
const codeSessionType = getCodeDisplayProtocol(displayProtocol, this.configuration.args['ozone-platform']);
if (releaseInfo) {
type ClientPlatformInfoClassification = {
platformId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system without any version information.' };
platformVersionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system version excluding any name information or release code.' };
platformIdLike: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system the current OS derivate is closely related to.' };
desktopEnvironment: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the desktop environment the user is using.' };
displayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the users display protocol type.' };
codeDisplayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the vscode display protocol type.' };
owner: 'benibenj';
comment: 'Provides insight into the distro and desktop environment information on Linux.';
};
@@ -481,12 +488,16 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
platformVersionId: string | undefined;
platformIdLike: string | undefined;
desktopEnvironment: string | undefined;
displayProtocol: string | undefined;
codeDisplayProtocol: string | undefined;
};
telemetryService.publicLog2<ClientPlatformInfoEvent, ClientPlatformInfoClassification>('clientPlatformInfo', {
platformId: releaseInfo.id,
platformVersionId: releaseInfo.version_id,
platformIdLike: releaseInfo.id_like,
desktopEnvironment: desktopEnvironment
desktopEnvironment: desktopEnvironment,
displayProtocol: displayProtocol,
codeDisplayProtocol: codeSessionType
});
}
}
@@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { Position } from 'vs/editor/common/core/position';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
import { TokenizationRegistry } from 'vs/editor/common/languages';
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry';
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme';
@@ -29,7 +29,9 @@ class Settings {
public readonly borderColor: string | null;
public readonly hideCursor: boolean;
public readonly cursorColor: string | null;
public readonly cursorColorSingle: string | null;
public readonly cursorColorPrimary: string | null;
public readonly cursorColorSecondary: string | null;
public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark';
public readonly backgroundColor: Color | null;
@@ -55,8 +57,12 @@ class Settings {
this.borderColor = borderColor ? borderColor.toString() : null;
this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler);
const cursorColor = theme.getColor(editorCursorForeground);
this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
const cursorColorSingle = theme.getColor(editorCursorForeground);
this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null;
const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground);
this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null;
const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground);
this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null;
this.themeType = theme.type;
@@ -189,7 +195,9 @@ class Settings {
&& this.renderBorder === other.renderBorder
&& this.borderColor === other.borderColor
&& this.hideCursor === other.hideCursor
&& this.cursorColor === other.cursorColor
&& this.cursorColorSingle === other.cursorColorSingle
&& this.cursorColorPrimary === other.cursorColorPrimary
&& this.cursorColorSecondary === other.cursorColorSecondary
&& this.themeType === other.themeType
&& Color.equals(this.backgroundColor, other.backgroundColor)
&& this.top === other.top
@@ -213,6 +221,11 @@ const enum OverviewRulerLane {
Full = 7
}
type Cursor = {
position: Position;
color: string | null;
};
const enum ShouldRenderValue {
NotNeeded = 0,
Maybe = 1,
@@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart {
private readonly _tokensColorTrackerListener: IDisposable;
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private _settings!: Settings;
private _cursorPositions: Position[];
private _cursorPositions: Cursor[];
private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
private _renderedCursorPositions: Position[] = [];
private _renderedCursorPositions: Cursor[] = [];
constructor(context: ViewContext) {
super(context);
@@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart {
}
});
this._cursorPositions = [new Position(1, 1)];
this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }];
}
public override dispose(): void {
@@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart {
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._cursorPositions = [];
for (let i = 0, len = e.selections.length; i < len; i++) {
this._cursorPositions[i] = e.selections[i].getPosition();
let color = this._settings.cursorColorSingle;
if (len > 1) {
color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary;
}
this._cursorPositions.push({ position: e.selections[i].getPosition(), color });
}
this._cursorPositions.sort(Position.compare);
this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position));
return this._markRenderingIsMaybeNeeded();
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
@@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart {
if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) {
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe) {
@@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart {
}
// Draw cursors
if (!this._settings.hideCursor && this._settings.cursorColor) {
if (!this._settings.hideCursor) {
const cursorHeight = (2 * this._settings.pixelRatio) | 0;
const halfCursorHeight = (cursorHeight / 2) | 0;
const cursorX = this._settings.x[OverviewRulerLane.Full];
const cursorW = this._settings.w[OverviewRulerLane.Full];
canvasCtx.fillStyle = this._settings.cursorColor;
let prevY1 = -100;
let prevY2 = -100;
let prevColor: string | null = null;
for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
const cursor = this._cursorPositions[i];
const color = this._cursorPositions[i].color;
if (!color) {
continue;
}
const cursor = this._cursorPositions[i].position;
let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0;
if (yCenter < halfCursorHeight) {
@@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart {
const y1 = yCenter - halfCursorHeight;
const y2 = y1 + cursorHeight;
if (y1 > prevY2 + 1) {
if (y1 > prevY2 + 1 || color !== prevColor) {
// flush prev
if (i !== 0) {
if (i !== 0 && prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
prevY1 = y1;
@@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart {
prevY2 = y2;
}
}
prevColor = color;
canvasCtx.fillStyle = color;
}
if (prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) {
@@ -35,6 +35,12 @@ class ViewCursorRenderData {
) { }
}
export enum CursorPlurality {
Single,
MultiPrimary,
MultiSecondary,
}
export class ViewCursor {
private readonly _context: ViewContext;
private readonly _domNode: FastDomNode<HTMLElement>;
@@ -47,11 +53,12 @@ export class ViewCursor {
private _isVisible: boolean;
private _position: Position;
private _pluralityClass: string;
private _lastRenderedContent: string;
private _renderData: ViewCursorRenderData | null;
constructor(context: ViewContext) {
constructor(context: ViewContext, plurality: CursorPlurality) {
this._context = context;
const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo);
@@ -73,6 +80,8 @@ export class ViewCursor {
this._domNode.setDisplay('none');
this._position = new Position(1, 1);
this._pluralityClass = '';
this.setPlurality(plurality);
this._lastRenderedContent = '';
this._renderData = null;
@@ -86,6 +95,23 @@ export class ViewCursor {
return this._position;
}
public setPlurality(plurality: CursorPlurality) {
switch (plurality) {
default:
case CursorPlurality.Single:
this._pluralityClass = '';
break;
case CursorPlurality.MultiPrimary:
this._pluralityClass = 'cursor-primary';
break;
case CursorPlurality.MultiSecondary:
this._pluralityClass = 'cursor-secondary';
break;
}
}
public show(): void {
if (!this._isVisible) {
this._domNode.setVisibility('inherit');
@@ -229,7 +255,7 @@ export class ViewCursor {
this._domNode.domNode.textContent = this._lastRenderedContent;
}
this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
this._domNode.setDisplay('block');
this._domNode.setTop(this._renderData.top);
@@ -7,10 +7,14 @@ import 'vs/css!./viewCursors';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry';
import {
editorCursorBackground, editorCursorForeground,
editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground,
editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground
} from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import * as viewEvents from 'vs/editor/common/viewEvents';
@@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart {
this._isVisible = false;
this._primaryCursor = new ViewCursor(this._context);
this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
this._secondaryCursors = [];
this._renderData = [];
@@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart {
}
// --- begin event handlers
public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean {
this._isComposingInput = true;
this._updateBlinking();
@@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart {
this._secondaryCursors.length !== secondaryPositions.length
|| (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit)
);
this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single);
this._primaryCursor.onCursorPositionChanged(position, pauseAnimation);
this._updateBlinking();
@@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart {
// Create new cursors
const addCnt = secondaryPositions.length - this._secondaryCursors.length;
for (let i = 0; i < addCnt; i++) {
const newCursor = new ViewCursor(this._context);
const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary);
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
this._secondaryCursors.push(newCursor);
}
@@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart {
return true;
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
@@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart {
}
}
}
// --- end blinking logic
private _updateDomClassName(): void {
@@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart {
}
registerThemingParticipant((theme, collector) => {
const caret = theme.getColor(editorCursorForeground);
if (caret) {
let caretBackground = theme.getColor(editorCursorBackground);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
type CursorTheme = {
foreground: string;
background: string;
class: string;
};
const cursorThemes: CursorTheme[] = [
{ class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground },
{ class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground },
{ class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground },
];
for (const cursorTheme of cursorThemes) {
const caret = theme.getColor(cursorTheme.foreground);
if (caret) {
let caretBackground = theme.getColor(cursorTheme.background);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
}
}
}
});
@@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight
export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.'));
export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorMultiCursorPrimaryForeground = registerColor('editorMultiCursor.primary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorPrimaryForeground', 'Color of the primary editor cursor when multiple cursors are present.'));
export const editorMultiCursorPrimaryBackground = registerColor('editorMultiCursor.primary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorPrimaryBackground', 'The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorMultiCursorSecondaryForeground = registerColor('editorMultiCursor.secondary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorSecondaryForeground', 'Color of secondary editor cursors when multiple cursors are present.'));
export const editorMultiCursorSecondaryBackground = registerColor('editorMultiCursor.secondary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorSecondaryBackground', 'The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.'));
export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.'));
@@ -36,8 +36,9 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry';
import { isHighContrast } from 'vs/platform/theme/common/theme';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types';
import { CodeActionModel, CodeActionsState } from './codeActionModel';
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel';
import { HierarchicalKind } from 'vs/base/common/hierarchicalKind';
interface IActionShowOptions {
@@ -291,7 +292,22 @@ export class CodeActionController extends Disposable implements IEditorContribut
if (token.isCancellationRequested) {
return;
}
return { canPreview: !!action.action.edit?.edits.length };
let canPreview = false;
const actionKind = action.action.kind;
if (actionKind) {
const hierarchicalKind = new HierarchicalKind(actionKind);
const refactorKinds = [
CodeActionKind.RefactorExtract,
CodeActionKind.RefactorInline,
CodeActionKind.RefactorRewrite
];
canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind));
}
return { canPreview: canPreview || !!action.action.edit?.edits.length };
},
onFocus: (action: CodeActionItem | undefined) => {
if (action && action.action) {
@@ -9,7 +9,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand';
import { EditorAutoIndentStrategy, EditorOption } from 'vs/editor/common/config/editorOptions';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContribution } from 'vs/editor/common/editorCommon';
@@ -20,123 +20,11 @@ import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules';
import { IModelService } from 'vs/editor/common/services/model';
import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils';
import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils';
import * as nls from 'vs/nls';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { normalizeIndentation } from 'vs/editor/common/core/indentation';
import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent';
export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] {
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
// Model is empty
return [];
}
const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules;
if (!indentationRules) {
return [];
}
endLineNumber = Math.min(endLineNumber, model.getLineCount());
// Skip `unIndentedLinePattern` lines
while (startLineNumber <= endLineNumber) {
if (!indentationRules.unIndentedLinePattern) {
break;
}
const text = model.getLineContent(startLineNumber);
if (!indentationRules.unIndentedLinePattern.test(text)) {
break;
}
startLineNumber++;
}
if (startLineNumber > endLineNumber - 1) {
return [];
}
const { tabSize, indentSize, insertSpaces } = model.getOptions();
const shiftIndent = (indentation: string, count?: number) => {
count = count || 1;
return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces);
};
const unshiftIndent = (indentation: string, count?: number) => {
count = count || 1;
return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces);
};
const indentEdits: ISingleEditOperation[] = [];
// indentation being passed to lines below
let globalIndent: string;
// Calculate indentation for the first line
// If there is no passed-in indentation, we use the indentation of the first line as base.
const currentLineText = model.getLineContent(startLineNumber);
let adjustedLineContent = currentLineText;
if (inheritedIndent !== undefined && inheritedIndent !== null) {
globalIndent = inheritedIndent;
const oldIndentation = strings.getLeadingWhitespace(currentLineText);
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
globalIndent = unshiftIndent(globalIndent);
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
}
if (currentLineText !== adjustedLineContent) {
indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces)));
}
} else {
globalIndent = strings.getLeadingWhitespace(currentLineText);
}
// idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`.
let idealIndentForNextLine: string = globalIndent;
if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
globalIndent = shiftIndent(globalIndent);
}
else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
}
startLineNumber++;
// Calculate indentation adjustment for all following lines
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const text = model.getLineContent(lineNumber);
const oldIndentation = strings.getLeadingWhitespace(text);
const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length);
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
idealIndentForNextLine = unshiftIndent(idealIndentForNextLine);
globalIndent = unshiftIndent(globalIndent);
}
if (oldIndentation !== idealIndentForNextLine) {
indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces)));
}
// calculate idealIndentForNextLine
if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) {
// In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines
// but don't change globalIndent and idealIndentForNextLine.
continue;
} else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
globalIndent = shiftIndent(globalIndent);
idealIndentForNextLine = globalIndent;
} else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
} else {
idealIndentForNextLine = globalIndent;
}
}
return indentEdits;
}
import { getReindentEditOperations } from '../common/indentation';
export class IndentationToSpacesAction extends EditorAction {
public static readonly ID = 'editor.action.indentationToSpaces';
@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { normalizeIndentation } from 'vs/editor/common/core/indentation';
import { Selection } from 'vs/editor/common/core/selection';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ITextModel } from 'vs/editor/common/model';
export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] {
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
// Model is empty
return [];
}
const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules;
if (!indentationRules) {
return [];
}
endLineNumber = Math.min(endLineNumber, model.getLineCount());
// Skip `unIndentedLinePattern` lines
while (startLineNumber <= endLineNumber) {
if (!indentationRules.unIndentedLinePattern) {
break;
}
const text = model.getLineContent(startLineNumber);
if (!indentationRules.unIndentedLinePattern.test(text)) {
break;
}
startLineNumber++;
}
if (startLineNumber > endLineNumber - 1) {
return [];
}
const { tabSize, indentSize, insertSpaces } = model.getOptions();
const shiftIndent = (indentation: string, count?: number) => {
count = count || 1;
return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces);
};
const unshiftIndent = (indentation: string, count?: number) => {
count = count || 1;
return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces);
};
const indentEdits: ISingleEditOperation[] = [];
// indentation being passed to lines below
let globalIndent: string;
// Calculate indentation for the first line
// If there is no passed-in indentation, we use the indentation of the first line as base.
const currentLineText = model.getLineContent(startLineNumber);
let adjustedLineContent = currentLineText;
if (inheritedIndent !== undefined && inheritedIndent !== null) {
globalIndent = inheritedIndent;
const oldIndentation = strings.getLeadingWhitespace(currentLineText);
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
globalIndent = unshiftIndent(globalIndent);
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
}
if (currentLineText !== adjustedLineContent) {
indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces)));
}
} else {
globalIndent = strings.getLeadingWhitespace(currentLineText);
}
// idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`.
let idealIndentForNextLine: string = globalIndent;
if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
globalIndent = shiftIndent(globalIndent);
}
else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
}
startLineNumber++;
// Calculate indentation adjustment for all following lines
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const text = model.getLineContent(lineNumber);
const oldIndentation = strings.getLeadingWhitespace(text);
const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length);
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
idealIndentForNextLine = unshiftIndent(idealIndentForNextLine);
globalIndent = unshiftIndent(globalIndent);
}
if (oldIndentation !== idealIndentForNextLine) {
indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces)));
}
// calculate idealIndentForNextLine
if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) {
// In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines
// but don't change globalIndent and idealIndentForNextLine.
continue;
} else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
globalIndent = shiftIndent(globalIndent);
idealIndentForNextLine = globalIndent;
} else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
idealIndentForNextLine = shiftIndent(idealIndentForNextLine);
} else {
idealIndentForNextLine = globalIndent;
}
}
return indentEdits;
}
@@ -6,19 +6,25 @@
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { EncodedTokenizationResult, IState, TokenizationRegistry } from 'vs/editor/common/languages';
import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes';
import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { NullState } from 'vs/editor/common/languages/nullTokenize';
import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/contrib/indentation/browser/indentation';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { testCommand } from 'vs/editor/test/browser/testCommand';
import { javascriptIndentationRules } from 'vs/editor/test/common/modes/supports/javascriptIndentationRules';
import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
enum Language {
TypeScript,
Ruby
}
function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void {
testCommand(lines, null, selection, (accessor, sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection);
@@ -28,7 +34,72 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab
testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection);
}
suite('Editor Contrib - Indentation to Spaces', () => {
function registerLanguage(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) {
const languageService = instantiationService.get(ILanguageService);
registerLanguageConfiguration(instantiationService, languageId, language, disposables);
disposables.add(languageService.registerLanguage({ id: languageId }));
}
// TODO@aiday-mar read directly the configuration file
function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) {
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
switch (language) {
case Language.TypeScript:
disposables.add(languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
indentationRules: javascriptIndentationRules,
onEnterRules: javascriptOnEnterRules
}));
break;
case Language.Ruby:
disposables.add(languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
indentationRules: {
decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b|(in|when)\s)/,
increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/,
},
}));
break;
}
}
function registerTokens(instantiationService: TestInstantiationService, tokens: { startIndex: number; value: number }[][], languageId: string, disposables: DisposableStore) {
let lineIndex = 0;
const languageService = instantiationService.get(ILanguageService);
const tokenizationSupport: ITokenizationSupport = {
getInitialState: () => NullState,
tokenize: undefined!,
tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {
const tokensOnLine = tokens[lineIndex++];
const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId);
const result = new Uint32Array(2 * tokensOnLine.length);
for (let i = 0; i < tokensOnLine.length; i++) {
result[2 * i] = tokensOnLine[i].startIndex;
result[2 * i + 1] =
(
(encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET)
| (tokensOnLine[i].value << MetadataConsts.TOKEN_TYPE_OFFSET)
);
}
return new EncodedTokenizationResult(result, state);
}
};
disposables.add(TokenizationRegistry.register(languageId, tokenizationSupport));
}
suite('Change Indentation to Spaces - TypeScript/Javascript', () => {
ensureNoDisposablesAreLeakedInTestSuite();
@@ -117,7 +188,7 @@ suite('Editor Contrib - Indentation to Spaces', () => {
});
});
suite('Editor Contrib - Indentation to Tabs', () => {
suite('Change Indentation to Tabs - TypeScript/Javascript', () => {
ensureNoDisposablesAreLeakedInTestSuite();
@@ -202,7 +273,9 @@ suite('Editor Contrib - Indentation to Tabs', () => {
});
});
suite('Editor Contrib - Auto Indent On Paste', () => {
suite('`Full` Auto Indent On Paste - TypeScript/JavaScript', () => {
const languageId = 'ts-test';
let disposables: DisposableStore;
setup(() => {
@@ -216,95 +289,61 @@ suite('Editor Contrib - Auto Indent On Paste', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('issue #119225: Do not add extra leading space when pasting JSDoc', () => {
const languageId = 'leadingSpacePaste';
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
const languageService = instantiationService.get(ILanguageService);
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
disposables.add(languageService.registerLanguage({ id: languageId }));
disposables.add(TokenizationRegistry.register(languageId, {
getInitialState: (): IState => NullState,
tokenize: () => {
throw new Error('not implemented');
},
tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {
const tokensArr: number[] = [];
if (line.indexOf('*') !== -1) {
tokensArr.push(0);
tokensArr.push(StandardTokenType.Comment << MetadataConsts.TOKEN_TYPE_OFFSET);
} else {
tokensArr.push(0);
tokensArr.push(StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET);
}
const tokens = new Uint32Array(tokensArr.length);
for (let i = 0; i < tokens.length; i++) {
tokens[i] = tokensArr[i];
}
return new EncodedTokenizationResult(tokens, state);
}
}));
disposables.add(languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
indentationRules: javascriptIndentationRules,
onEnterRules: javascriptOnEnterRules
}));
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
const pasteText = [
'/**',
' * JSDoc',
' */',
'function a() {}'
].join('\n');
const tokens = [
[
{ startIndex: 0, value: 1 },
{ startIndex: 3, value: 1 },
],
[
{ startIndex: 0, value: 1 },
{ startIndex: 2, value: 1 },
{ startIndex: 8, value: 1 },
],
[
{ startIndex: 0, value: 1 },
{ startIndex: 1, value: 1 },
{ startIndex: 3, value: 0 },
],
[
{ startIndex: 0, value: 0 },
{ startIndex: 8, value: 0 },
{ startIndex: 9, value: 0 },
{ startIndex: 10, value: 0 },
{ startIndex: 11, value: 0 },
{ startIndex: 12, value: 0 },
{ startIndex: 13, value: 0 },
{ startIndex: 14, value: 0 },
{ startIndex: 15, value: 0 },
]
];
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
registerTokens(instantiationService, tokens, languageId, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(pasteText, true, undefined, 'keyboard');
autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16));
assert.strictEqual(model.getValue(), pasteText);
});
});
});
suite('Editor Contrib - Keep Indent On Paste', () => {
let disposables: DisposableStore;
setup(() => {
disposables = new DisposableStore();
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
test('issue #167299: Blank line removes indent', () => {
const languageId = 'blankLineRemovesIndent';
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
const languageService = instantiationService.get(ILanguageService);
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
disposables.add(languageService.registerLanguage({ id: languageId }));
disposables.add(languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
indentationRules: javascriptIndentationRules,
onEnterRules: javascriptOnEnterRules
}));
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
// no need for tokenization because there are no comments
const pasteText = [
'',
'export type IncludeReference =',
@@ -319,14 +358,393 @@ suite('Editor Contrib - Keep Indent On Paste', () => {
'}'
].join('\n');
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(pasteText, true, undefined, 'keyboard');
autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2));
assert.strictEqual(model.getValue(), pasteText);
});
});
// Failing tests found in issues...
test.skip('issue #181065: Incorrect paste of object within comment', () => {
// https://github.com/microsoft/vscode/issues/181065
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
const text = [
'/**',
' * @typedef {',
' * }',
' */'
].join('\n');
const tokens = [
[
{ startIndex: 0, value: 1 },
{ startIndex: 3, value: 1 },
],
[
{ startIndex: 0, value: 1 },
{ startIndex: 2, value: 1 },
{ startIndex: 3, value: 1 },
{ startIndex: 11, value: 1 },
{ startIndex: 12, value: 0 },
{ startIndex: 13, value: 0 },
],
[
{ startIndex: 0, value: 1 },
{ startIndex: 2, value: 0 },
{ startIndex: 3, value: 0 },
{ startIndex: 4, value: 0 },
],
[
{ startIndex: 0, value: 1 },
{ startIndex: 1, value: 1 },
{ startIndex: 3, value: 0 },
]
];
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
registerTokens(instantiationService, tokens, languageId, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(text, true, undefined, 'keyboard');
autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4));
assert.strictEqual(model.getValue(), text);
});
});
test.skip('issue #86301: preserve cursor at inserted indentation level', () => {
// https://github.com/microsoft/vscode/issues/86301
const model = createTextModel([
'() => {',
'',
'}',
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
editor.setSelection(new Selection(2, 1, 2, 1));
const text = [
'() => {',
'',
'}',
''
].join('\n');
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(text, true, undefined, 'keyboard');
autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1));
// notes:
// why is line 3 not indented to the same level as line 2?
// looks like the indentation is inserted correctly at line 5, but the cursor does not appear at the maximum indentation level?
assert.strictEqual(model.getValue(), [
'() => {',
' () => {',
' ', // <- should also be indented
' }',
' ', // <- cursor should be at the end of the indentation
'}',
].join('\n'));
const selection = viewModel.getSelection();
assert.deepStrictEqual(selection, new Selection(5, 5, 5, 5));
});
});
test.skip('issue #85781: indent line with extra white space', () => {
// https://github.com/microsoft/vscode/issues/85781
// note: still to determine whether this is a bug or not
const model = createTextModel([
'() => {',
' console.log("a");',
'}',
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
editor.setSelection(new Selection(2, 5, 2, 5));
const text = [
'() => {',
' console.log("b")',
'}',
' '
].join('\n');
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(text, true, undefined, 'keyboard');
// todo@aiday-mar, make sure range is correct, and make test work as in real life
autoIndentOnPasteController.trigger(new Range(2, 5, 5, 6));
assert.strictEqual(model.getValue(), [
'() => {',
' () => {',
' console.log("b")',
' }',
' console.log("a");',
'}',
].join('\n'));
});
});
test.skip('issue #29589: incorrect indentation of closing brace on paste', () => {
// https://github.com/microsoft/vscode/issues/29589
const model = createTextModel('', languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => {
editor.setSelection(new Selection(2, 5, 2, 5));
const text = [
'function makeSub(a,b) {',
'subsent = sent.substring(a,b);',
'return subsent;',
'}',
].join('\n');
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste);
viewModel.paste(text, true, undefined, 'keyboard');
// todo@aiday-mar, make sure range is correct, and make test work as in real life
autoIndentOnPasteController.trigger(new Range(1, 1, 4, 2));
assert.strictEqual(model.getValue(), [
'function makeSub(a,b) {',
' subsent = sent.substring(a,b);',
' return subsent;',
'}',
].join('\n'));
});
});
});
suite('Editor Contrib - Auto Dedent On Type', () => {
suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
const languageId = "ts-test";
let disposables: DisposableStore;
setup(() => {
disposables = new DisposableStore();
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
// Test added so there is at least one non-ignored test in this suite
test('temporary test', () => {
assert.ok(true);
});
// Failing tests from issues...
test.skip('issue #208215: indent after arrow function', () => {
// https://github.com/microsoft/vscode/issues/208215
// consider the regex: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
viewModel.type('const add1 = (n) =>');
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'const add1 = (n) =>',
' ',
].join('\n'));
});
});
test.skip('issue #208215: outdented after semicolon detected after arrow function', () => {
// Notes: we want to outdent after having detected a semi-colon which marks the end of the line, but only when we have detected an arrow function
// We could use one outdent pattern corresponding per indent pattern, and not a generic outdent and indent pattern
const model = createTextModel([
'const add1 = (n) =>',
' console.log("hi");',
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(2, 24, 2, 24));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'const add1 = (n) =>',
' console.log("hi");',
'',
].join('\n'));
});
});
test.skip('issue #116843: indent after arrow function', () => {
// https://github.com/microsoft/vscode/issues/116843
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
viewModel.type([
'const add1 = (n) =>',
' n + 1;',
].join('\n'));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'const add1 = (n) =>',
' n + 1;',
'',
].join('\n'));
});
});
test.skip('issue #40115: keep indentation when added', () => {
// https://github.com/microsoft/vscode/issues/40115
const model = createTextModel('function foo() {}', languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(1, 17, 1, 17));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'function foo() {',
' ',
'}',
].join('\n'));
editor.setSelection(new Selection(2, 5, 2, 5));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'function foo() {',
' ',
' ',
'}',
].join('\n'));
});
});
test.skip('issue #193875: incorrect indentation on enter', () => {
// https://github.com/microsoft/vscode/issues/193875
const model = createTextModel([
'{',
' for(;;)',
' for(;;) {}',
'}',
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(3, 14, 3, 14));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'{',
' for(;;)',
' for(;;) {',
' ',
' }',
'}',
].join('\n'));
});
});
test.skip('issue #43244: incorrect indentation', () => {
// https://github.com/microsoft/vscode/issues/43244
// potential regex to fix: "^.*[if|while|for]\s*\(.*\)\s*",
const model = createTextModel([
'function f() {',
' if (condition)',
' return;',
'}'
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(3, 16, 3, 16));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'function f() {',
' if (condition)',
' return;',
'',
'}',
].join('\n'));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'function f() {',
' if (condition)',
' return;',
'',
'',
'}',
].join('\n'));
});
});
test.skip('issue #208232: incorrect indentation inside of comments', () => {
// https://github.com/microsoft/vscode/issues/208232
const model = createTextModel([
'/**',
'indentation done for {',
'*/'
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(2, 23, 2, 23));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'/**',
'indentation done for {',
'',
'*/'
].join('\n'));
});
});
// Add tests for:
// https://github.com/microsoft/vscode/issues/88638
// https://github.com/microsoft/vscode/issues/63388
// https://github.com/microsoft/vscode/issues/46401
// https://github.com/microsoft/vscode/issues/174044
});
suite('Auto Indent On Type - Ruby', () => {
const languageId = "ruby-test";
let disposables: DisposableStore;
setup(() => {
@@ -340,45 +758,14 @@ suite('Editor Contrib - Auto Dedent On Type', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('issue #198350: in or when incorrectly match non keywords for Ruby', () => {
const languageId = "ruby";
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
const languageService = instantiationService.get(ILanguageService);
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
disposables.add(languageService.registerLanguage({ id: languageId }));
const languageModel = languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
indentationRules: {
decreaseIndentPattern: /\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when|in)\b)/,
increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/,
},
});
viewModel.type("def foo\n i", 'keyboard');
viewModel.type("n", 'keyboard');
// The 'in' triggers decreaseIndentPattern immediately, which is incorrect
assert.strictEqual(model.getValue(), "def foo\nin");
languageModel.dispose();
registerLanguage(instantiationService, languageId, Language.Ruby, disposables);
const improvedLanguageModel = languageConfigurationService.register(languageId, {
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
indentationRules: {
decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b|(in|when)\s)/,
increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/,
},
});
viewModel.model.setValue("");
viewModel.type("def foo\n i");
viewModel.type("n", 'keyboard');
assert.strictEqual(model.getValue(), "def foo\n in");
@@ -390,7 +777,6 @@ suite('Editor Contrib - Auto Dedent On Type', () => {
assert.strictEqual(model.getValue(), " # in");
viewModel.type(" ", 'keyboard');
assert.strictEqual(model.getValue(), " # in ");
improvedLanguageModel.dispose();
});
});
});
@@ -13,7 +13,7 @@ import { ITextModel } from 'vs/editor/common/model';
import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/languageConfiguration';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules';
import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils';
import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils';
import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from 'vs/editor/common/languages/autoIndent';
import { getEnterAction } from 'vs/editor/common/languages/enterAction';
+2 -2
View File
@@ -237,9 +237,9 @@ export class LinkDetector extends Disposable implements IEditorContribution {
const fsPath = resources.originalFSPath(parsedUri);
let relativePath: string | null = null;
if (fsPath.startsWith('/./')) {
if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) {
relativePath = `.${fsPath.substr(1)}`;
} else if (fsPath.startsWith('//./')) {
} else if (fsPath.startsWith('//./') || fsPath.startsWith('\\\\.\\')) {
relativePath = `.${fsPath.substr(2)}`;
}
+11 -15
View File
@@ -38,7 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputField, RenameInputFieldResult } from './renameInputField';
import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputFieldResult, RenameWidget } from './renameInputField';
class RenameSkeleton {
@@ -138,7 +138,7 @@ class RenameController implements IEditorContribution {
return editor.getContribution<RenameController>(RenameController.ID);
}
private readonly _renameInputField: RenameInputField;
private readonly _renameInputField: RenameWidget;
private readonly _disposableStore = new DisposableStore();
private _cts: CancellationTokenSource = new CancellationTokenSource();
@@ -153,7 +153,7 @@ class RenameController implements IEditorContribution {
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
) {
this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));
this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));
}
dispose(): void {
@@ -229,23 +229,19 @@ class RenameController implements IEditorContribution {
const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled
const renameCandidatesCts = new CancellationTokenSource(cts2.token);
const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model);
const newSymbolNameProvidersResults = newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, renameCandidatesCts.token));
trace(`requested new symbol names from ${newSymbolNamesProviders.length} providers`);
const selection = this.editor.getSelection();
let selectionStart = 0;
let selectionEnd = loc.text.length;
if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(loc.range, selection)) {
selectionStart = Math.max(0, selection.startColumn - loc.range.startColumn);
selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn;
}
const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts));
trace('creating rename input field and awaiting its result');
const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview');
const inputFieldResult = await this._renameInputField.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, newSymbolNameProvidersResults, renameCandidatesCts);
const inputFieldResult = await this._renameInputField.getInput(
loc.range,
loc.text,
supportPreview,
requestRenameSuggestions,
cts2
);
trace('received response from rename input field');
if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently
@@ -23,7 +23,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { IDimension } from 'vs/editor/common/core/dimension';
import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages';
import { localize } from 'vs/nls';
@@ -85,7 +85,13 @@ interface IRenameInputField {
/**
* @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult}
*/
getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult<NewSymbolName[]>[], cts: CancellationTokenSource): Promise<RenameInputFieldResult | boolean>;
getInput(
where: IRange,
currentName: string,
supportPreview: boolean,
requestRenameSuggestions: (cts: CancellationToken) => ProviderResult<NewSymbolName[]>[],
cts: CancellationTokenSource
): Promise<RenameInputFieldResult | boolean>;
acceptInput(wantsPreview: boolean): void;
cancelInput(focusEditor: boolean, caller: string): void;
@@ -94,7 +100,7 @@ interface IRenameInputField {
focusPreviousRenameSuggestion(): void;
}
export class RenameInputField implements IRenameInputField, IContentWidget, IDisposable {
export class RenameWidget implements IRenameInputField, IContentWidget, IDisposable {
// implement IContentWidget
readonly allowEditorOverflow: boolean = true;
@@ -127,6 +133,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
*/
private _timeBeforeFirstInputFieldEdit: number | undefined;
private _renameCandidateProvidersCts: CancellationTokenSource | undefined;
private readonly _visibleContextKey: IContextKey<boolean>;
private readonly _disposables = new DisposableStore();
@@ -194,6 +202,9 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
this._isEditingRenameCandidate = true;
}
this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed();
if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) {
this._renameCandidateProvidersCts.cancel();
}
this._renameCandidateListView?.clearFocus();
})
);
@@ -346,7 +357,19 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
}
}
getInput(where: IRange, currentName: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult<NewSymbolName[]>[], cts: CancellationTokenSource): Promise<RenameInputFieldResult | boolean> {
getInput(
where: IRange,
currentName: string,
supportPreview: boolean,
requestRenameSuggestions: (cts: CancellationToken) => ProviderResult<NewSymbolName[]>[],
cts: CancellationTokenSource
): Promise<RenameInputFieldResult | boolean> {
const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName);
this._renameCandidateProvidersCts = new CancellationTokenSource();
const candidates = requestRenameSuggestions(this._renameCandidateProvidersCts.token);
this._updateRenameCandidates(candidates, currentName, cts.token);
this._isEditingRenameCandidate = false;
@@ -365,8 +388,12 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
const disposeOnDone = new DisposableStore();
disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call
this._updateRenameCandidates(candidates, currentName, cts.token);
disposeOnDone.add(toDisposable(() => {
if (this._renameCandidateProvidersCts !== undefined) {
this._renameCandidateProvidersCts.dispose(true);
this._renameCandidateProvidersCts = undefined;
}
}));
const inputResult = new DeferredPromise<RenameInputFieldResult | boolean>();
@@ -433,6 +460,24 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
return inputResult.p;
}
/**
* This allows selecting only part of the symbol name in the input field based on the selection in the editor
*/
private _getSelection(where: IRange, currentName: string): { start: number; end: number } {
assertType(this._editor.hasModel());
const selection = this._editor.getSelection();
let start = 0;
let end = currentName.length;
if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(where, selection)) {
start = Math.max(0, selection.startColumn - where.startColumn);
end = Math.min(where.endColumn, selection.endColumn) - where.startColumn;
}
return { start, end };
}
private _show(): void {
this._trace('invoking _show');
this._editor.revealLineInCenterIfOutsideViewport(this._position!.lineNumber, ScrollType.Smooth);
@@ -508,8 +553,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
return this._editor.getTopForLineNumber(this._position!.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport);
}
private _trace(...args: any[]) {
this._logService.trace('RenameInputField', ...args);
private _trace(...args: unknown[]) {
this._logService.trace('RenameWidget', ...args);
}
}
@@ -55,7 +55,7 @@ export interface IActionDescriptor {
*/
label: string;
/**
* Precondition rule.
* Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts).
*/
precondition?: string;
/**
@@ -29,5 +29,15 @@ export const javascriptOnEnterRules = [
// e.g. *-----*/|
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: { indentAction: IndentAction.None, removeText: 1 }
}
},
{
beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/,
afterText: /^(?!\s*(\bcase\b|\bdefault\b))/,
action: { indentAction: IndentAction.Indent }
},
{
previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/,
beforeText: /^\s+([^{i\s]|i(?!f\b))/,
action: { indentAction: IndentAction.Outdent }
},
];
+1 -1
View File
@@ -1251,7 +1251,7 @@ declare namespace monaco.editor {
*/
label: string;
/**
* Precondition rule.
* Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts).
*/
precondition?: string;
/**
@@ -140,4 +140,5 @@ export interface NativeParsedArgs {
'log-net-log'?: string;
'vmodule'?: string;
'disable-dev-shm-usage'?: boolean;
'ozone-platform'?: string;
}
+1
View File
@@ -204,6 +204,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'_urls': { type: 'string[]' },
'disable-dev-shm-usage': { type: 'boolean' },
'profile-temp': { type: 'boolean' },
'ozone-platform': { type: 'string' },
_: { type: 'string[]' } // main arguments
};
@@ -59,7 +59,7 @@
.quick-input-header {
display: flex;
padding: 8px 6px 6px 6px;
padding: 8px 6px 2px 6px;
}
.quick-input-widget.hidden-input .quick-input-header {
@@ -263,11 +263,16 @@
overflow: hidden;
}
.quick-input-list .monaco-highlighted-label .highlight {
/* preserve list-like styling instead of tree-like styling */
.quick-input-list .monaco-list .monaco-list-row .monaco-highlighted-label .highlight {
font-weight: bold;
/* preserve list-like styling instead of tree-like styling */
background-color: unset;
color: var(--vscode-list-highlightForeground) !important;
}
/* preserve list-like styling instead of tree-like styling */
.quick-input-list .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight {
color: var(--vscode-list-focusHighlightForeground) !important;
background-color: transparent;
}
.quick-input-list .quick-input-list-entry .quick-input-list-separator {
@@ -318,12 +323,21 @@
background: none;
}
/* Quick input separators as full-row item */
.quick-input-list .quick-input-list-separator-as-item {
font-weight: 600;
padding: 4px 6px;
font-size: 12px;
}
/* Quick input separators as full-row item */
.quick-input-list .quick-input-list-separator-as-item .label-name {
font-weight: 600;
}
.quick-input-list .quick-input-list-separator-as-item .label-description {
/* Override default description opacity so we don't have a contrast ratio issue. */
opacity: 1 !important;
}
/* Hide border when the item becomes the sticky one */
.quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border {
border-top-style: none;
@@ -92,6 +92,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
}
}
// Store the existing selection if there was one.
const visibleSelection = visibleQuickAccess?.picker?.valueSelection;
const visibleValue = visibleQuickAccess?.picker?.value;
// Create a picker for the provider to use with the initial value
// and adjust the filtering to exclude the prefix from filtering
const disposables = new DisposableStore();
@@ -148,6 +152,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
// on the onDidHide event.
picker.show();
// If the previous picker had a selection and the value is unchanged, we should set that in the new picker.
if (visibleSelection && visibleValue === value) {
picker.valueSelection = visibleSelection;
}
// Pick mode: return with promise
if (pick) {
return pickPromise?.p;
@@ -723,7 +723,15 @@ export class QuickPick<T extends IQuickPickItem> extends QuickInput implements I
return this.ui.keyMods;
}
set valueSelection(valueSelection: Readonly<[number, number]>) {
get valueSelection() {
const selection = this.ui.inputBox.getSelection();
if (!selection) {
return undefined;
}
return [selection.start, selection.end];
}
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
this._valueSelection = valueSelection;
this.valueSelectionUpdated = true;
this.update();
@@ -1167,7 +1175,15 @@ export class InputBox extends QuickInput implements IInputBox {
this.update();
}
set valueSelection(valueSelection: Readonly<[number, number]>) {
get valueSelection() {
const selection = this.ui.inputBox.getSelection();
if (!selection) {
return undefined;
}
return [selection.start, selection.end];
}
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
this._valueSelection = valueSelection;
this.valueSelectionUpdated = true;
this.update();
@@ -59,6 +59,10 @@ export class QuickInputBox extends Disposable {
this.findInput.inputBox.select(range);
}
getSelection(): IRange | null {
return this.findInput.inputBox.getSelection();
}
isSelectionAtEnd(): boolean {
return this.findInput.inputBox.isSelectionAtEnd();
}
@@ -110,6 +110,8 @@ class BaseQuickPickItemElement implements IQuickPickElement {
saneAriaLabel
};
});
this._saneDescription = mainItem.description;
this._saneTooltip = mainItem.tooltip;
}
// #region Lazy Getters
@@ -144,7 +146,7 @@ class BaseQuickPickItemElement implements IQuickPickElement {
this._hidden = value;
}
protected _saneDescription?: string;
private _saneDescription?: string;
get saneDescription() {
return this._saneDescription;
}
@@ -160,7 +162,7 @@ class BaseQuickPickItemElement implements IQuickPickElement {
this._saneDetail = value;
}
protected _saneTooltip?: string | IMarkdownString | HTMLElement;
private _saneTooltip?: string | IMarkdownString | HTMLElement;
get saneTooltip() {
return this._saneTooltip;
}
@@ -210,9 +212,7 @@ class QuickPickItemElement extends BaseQuickPickItemElement {
? Event.map(Event.filter<{ element: IQuickPickElement; checked: boolean }>(this._onChecked.event, e => e.element === this), e => e.checked)
: Event.None;
this._saneDescription = item.description;
this._saneDetail = item.detail;
this._saneTooltip = this.item.tooltip;
this._labelHighlights = item.highlights?.label;
this._descriptionHighlights = item.highlights?.description;
this._detailHighlights = item.highlights?.detail;
@@ -277,9 +277,8 @@ class QuickPickSeparatorElement extends BaseQuickPickItemElement {
class QuickInputItemDelegate implements IListVirtualDelegate<IQuickPickElement> {
getHeight(element: IQuickPickElement): number {
if (!element.item) {
// must be a separator
return 24;
if (element instanceof QuickPickSeparatorElement) {
return 30;
}
return element.saneDetail ? 44 : 22;
}
@@ -57,6 +57,7 @@ export interface IQuickPickSeparator {
type: 'separator';
id?: string;
label?: string;
description?: string;
ariaLabel?: string;
buttons?: readonly IQuickInputButton[];
tooltip?: string | IMarkdownString;
@@ -11,7 +11,7 @@ import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValida
import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint';
import { IconExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint';
import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint';
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint';
import { StatusBarItemsExtensionPoint } from 'vs/workbench/api/browser/statusBarExtensionPoint';
// --- mainThread participants
@@ -20,17 +20,17 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
type AgentData = {
interface AgentData {
dispose: () => void;
name: string;
id: string;
extensionId: ExtensionIdentifier;
hasFollowups?: boolean;
};
}
@extHostNamedCustomer(MainContext.MainThreadChatAgents2)
export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 {
@@ -48,7 +48,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IChatContributionService private readonly _chatContributionService: IChatContributionService,
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
@@ -59,7 +58,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._register(this._chatService.onDidPerformUserAction(e => {
if (typeof e.agentId === 'string') {
for (const [handle, agent] of this._agents) {
if (agent.name === e.agentId) {
if (agent.id === e.agentId) {
if (e.action.kind === 'vote') {
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction);
} else {
@@ -76,10 +75,16 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._agents.deleteAndDispose(handle);
}
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void {
const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name);
if (!staticAgentRegistration && !allowDynamic) {
throw new Error(`chatParticipant must be declared in package.json: ${name}`);
$registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void {
const staticAgentRegistration = this._chatAgentService.getAgent(id);
if (!staticAgentRegistration && !dynamicProps) {
if (this._chatAgentService.getAgentsByName(id).length) {
// Likely some extension authors will not adopt the new ID, so give a hint if they register a
// participant by name instead of ID.
throw new Error(`chatParticipant must be declared with an ID in package.json. The "id" property may be missing! "${id}"`);
}
throw new Error(`chatParticipant must be declared in package.json: ${id}`);
}
const impl: IChatAgentImplementation = {
@@ -107,10 +112,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
};
let disposable: IDisposable;
if (!staticAgentRegistration && allowDynamic) {
if (!staticAgentRegistration && dynamicProps) {
disposable = this._chatAgentService.registerDynamicAgent(
{
id: name,
id,
name: dynamicProps.name,
description: dynamicProps.description,
extensionId: extension,
metadata: revive(metadata),
slashCommands: [],
@@ -118,11 +125,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
},
impl);
} else {
disposable = this._chatAgentService.registerAgent(name, impl);
disposable = this._chatAgentService.registerAgentImplementation(id, impl);
}
this._agents.set(handle, {
name,
id: id,
extensionId: extension,
dispose: disposable.dispose,
hasFollowups: metadata.hasFollowups
});
@@ -134,7 +142,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
throw new Error(`No agent with handle ${handle} registered`);
}
data.hasFollowups = metadataUpdate.hasFollowups;
this._chatAgentService.updateAgent(data.name, revive(metadataUpdate));
this._chatAgentService.updateAgent(data.id, revive(metadataUpdate));
}
async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise<number | void> {
@@ -162,8 +170,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts;
const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
const thisAgentName = this._agents.get(handle)?.name;
if (agentPart?.agent.id !== thisAgentName) {
const thisAgentId = this._agents.get(handle)?.id;
if (agentPart?.agent.id !== thisAgentId) {
return;
}
@@ -1427,10 +1427,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider);
},
createChatParticipant(name: string, handler: vscode.ChatExtendedRequestHandler) {
createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) {
checkProposedApiEnabled(extension, 'chatParticipant');
return extHostChatAgents2.createChatAgent(extension, name, handler);
return extHostChatAgents2.createChatAgent(extension, id, handler);
},
createDynamicChatParticipant(id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
checkProposedApiEnabled(extension, 'chatParticipantAdditions');
return extHostChatAgents2.createDynamicChatAgent(extension, id, name, description, handler);
}
};
// namespace: lm
@@ -1209,7 +1209,7 @@ export interface IExtensionChatAgentMetadata extends Dto<IChatAgentMetadata> {
}
export interface MainThreadChatAgentsShape2 extends IDisposable {
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void;
$registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void;
$registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void;
$unregisterAgentCompletionsProvider(handle: number): void;
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;
@@ -122,7 +122,7 @@ class ChatAgentResponseStream {
},
push(part) {
throwIfDone(this.push);
const dto = typeConvert.ChatResponsePart.to(part);
const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables);
_report(dto);
return this;
},
@@ -166,12 +166,21 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2);
}
createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
const handle = ExtHostChatAgents2._idPool++;
const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler);
const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler);
this._agents.set(handle, agent);
this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions'));
this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined);
return agent.apiAgent;
}
createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
const handle = ExtHostChatAgents2._idPool++;
const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler);
this._agents.set(handle, agent);
this._proxy.$registerAgent(handle, extension.identifier, id, {}, { name, description });
return agent.apiAgent;
}
@@ -231,11 +240,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
{ ...ehResult, metadata: undefined };
// REQUEST turn
res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), { extensionId: '', name: h.request.agentId }));
res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId));
// RESPONSE turn
const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter)));
res.push(new extHostTypes.ChatResponseTurn(parts, result, { extensionId: '', name: h.request.agentId }, h.request.command));
res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command));
}
return res;
@@ -338,7 +347,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
class ExtHostChatAgent {
private _followupProvider: vscode.ChatFollowupProvider | undefined;
private _description: string | undefined;
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
private _isDefault: boolean | undefined;
@@ -437,7 +445,6 @@ class ExtHostChatAgent {
updateScheduled = true;
queueMicrotask(() => {
this._proxy.$updateAgent(this._handle, {
description: this._description,
fullName: this._fullName,
icon: !this._iconPath ? undefined :
this._iconPath instanceof URI ? this._iconPath :
@@ -463,16 +470,9 @@ class ExtHostChatAgent {
const that = this;
return {
get name() {
get id() {
return that.id;
},
get description() {
return that._description ?? '';
},
set description(v) {
that._description = v;
updateMetadataSoon();
},
get fullName() {
checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
return that._fullName ?? that.extension.displayName ?? that.extension.name;
@@ -109,8 +109,13 @@ class ChatVariableResolverResponseStream {
},
push(part) {
throwIfDone(this.push);
const dto = typeConvert.ChatResponsePart.to(part);
_report(dto as IChatVariableResolverProgressDto);
if (part instanceof extHostTypes.ChatResponseReferencePart) {
_report(typeConvert.ChatResponseReferencePart.to(part));
} else if (part instanceof extHostTypes.ChatResponseProgressPart) {
_report(typeConvert.ChatResponseProgressPart.to(part));
}
return this;
}
};
@@ -2454,7 +2454,7 @@ export namespace ChatResponseReferencePart {
export namespace ChatResponsePart {
export function to(part: vscode.ChatResponsePart): extHostProtocol.IChatProgressDto {
export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto {
if (part instanceof types.ChatResponseMarkdownPart) {
return ChatResponseMarkdownPart.to(part);
} else if (part instanceof types.ChatResponseAnchorPart) {
@@ -2465,6 +2465,8 @@ export namespace ChatResponsePart {
return ChatResponseProgressPart.to(part);
} else if (part instanceof types.ChatResponseFileTreePart) {
return ChatResponseFilesPart.to(part);
} else if (part instanceof types.ChatResponseCommandButtonPart) {
return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables);
}
return {
kind: 'content',
@@ -2546,7 +2548,7 @@ export namespace ChatResponseProgress {
};
} else if ('participant' in progress) {
checkProposedApiEnabled(extension, 'chatParticipantAdditions');
return { agentName: progress.participant, command: progress.command, kind: 'agentDetection' };
return { agentId: progress.participant, command: progress.command, kind: 'agentDetection' };
} else if ('message' in progress) {
return { content: MarkdownString.from(progress.message), kind: 'progressMessage' };
} else {
+2 -2
View File
@@ -4310,7 +4310,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn {
readonly prompt: string,
readonly command: string | undefined,
readonly variables: vscode.ChatResolvedVariable[],
readonly participant: { extensionId: string; name: string },
readonly participant: string,
) { }
}
@@ -4319,7 +4319,7 @@ export class ChatResponseTurn implements vscode.ChatResponseTurn {
constructor(
readonly response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>,
readonly result: vscode.ChatResult,
readonly participant: { extensionId: string; name: string },
readonly participant: string,
readonly command?: string
) { }
}
@@ -148,7 +148,7 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut
}
private async enableEntitlements(session: AuthenticationSession) {
const isInternal = isInternalTelemetry(this.productService, this.configurationService) ?? true;
const isInternal = isInternalTelemetry(this.productService, this.configurationService);
const showAccountsBadge = this.configurationService.inspect<boolean>(accountsBadgeConfigKey).value ?? false;
const showWelcomeView = this.configurationService.inspect<boolean>(chatWelcomeViewConfigKey).value ?? false;
@@ -128,7 +128,7 @@ export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 {
if (widget.getInput().match(/^\s*@/)) {
widget.acceptInput();
} else {
widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.id}`);
widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`);
}
}
}
@@ -251,7 +251,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
executeImmediately: true
}, async (prompt, progress) => {
const defaultAgent = chatAgentService.getDefaultAgent();
const agents = chatAgentService.getRegisteredAgents();
const agents = chatAgentService.getAgents();
// Report prefix
if (defaultAgent?.metadata.helpTextPrefix) {
@@ -270,7 +270,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
const agentWithLeader = `${chatAgentLeader}${a.id}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`;
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.description}`;
const commandText = a.slashCommands.map(c => {
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
@@ -121,6 +121,7 @@ export interface IChatWidget {
readonly providerId: string;
readonly supportsFileReferences: boolean;
readonly parsedInput: IParsedChatRequest;
lastSelectedAgent: IChatAgentData | undefined;
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined;
reveal(item: ChatTreeItem): void;
@@ -3,11 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { Codicon } from 'vs/base/common/codicons';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { localize, localize2 } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
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 { IProductService } from 'vs/platform/product/common/productService';
@@ -20,7 +22,8 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat
import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions';
import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
@@ -64,20 +67,24 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
extensionPoint: 'chatParticipants',
jsonSchema: {
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'),
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'),
type: 'array',
items: {
additionalProperties: false,
type: 'object',
defaultSnippets: [{ body: { name: '', description: '' } }],
required: ['name'],
required: ['name', 'id'],
properties: {
id: {
description: localize('chatParticipantId', "A unique id for this chat participant."),
type: 'string'
},
name: {
description: localize('chatParticipantName', "Unique name for this Chat Participant."),
description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."),
type: 'string'
},
description: {
description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."),
description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."),
type: 'string'
},
isDefault: {
@@ -92,7 +99,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
}
},
commands: {
markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."),
markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."),
type: 'array',
items: {
additionalProperties: false,
@@ -131,7 +138,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
}
},
locations: {
markdownDescription: localize('chatLocationsDescription', "Locations in which this Chat Participant is available."),
markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."),
type: 'array',
default: ['panel'],
items: {
@@ -158,12 +165,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
private _welcomeViewDescriptor?: IViewDescriptor;
private _viewContainer: ViewContainer;
private _registrationDisposables = new Map<string, IDisposable>();
private _participantRegistrationDisposables = new DisposableMap<string>();
constructor(
@IChatContributionService readonly _chatContributionService: IChatContributionService,
@IProductService readonly productService: IProductService,
@IContextKeyService readonly contextService: IContextKeyService,
@ILogService readonly logService: ILogService,
@IChatContributionService private readonly _chatContributionService: IChatContributionService,
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
@IProductService private readonly productService: IProductService,
@IContextKeyService private readonly contextService: IContextKeyService,
@ILogService private readonly logService: ILogService,
) {
this._viewContainer = this.registerViewContainer();
this.registerListeners();
@@ -243,13 +252,34 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
continue;
}
this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier });
if (!providerDescriptor.id || !providerDescriptor.name) {
this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`);
continue;
}
this._participantRegistrationDisposables.set(
getParticipantKey(extension.description.identifier, providerDescriptor.name),
this._chatAgentService.registerAgent(
providerDescriptor.id,
{
extensionId: extension.description.identifier,
id: providerDescriptor.id,
description: providerDescriptor.description,
metadata: {},
name: providerDescriptor.name,
isDefault: providerDescriptor.isDefault,
defaultImplicitVariables: providerDescriptor.defaultImplicitVariables,
locations: isNonEmptyArray(providerDescriptor.locations) ?
providerDescriptor.locations.map(ChatAgentLocation.fromRaw) :
[ChatAgentLocation.Panel],
slashCommands: providerDescriptor.commands ?? []
} satisfies IChatAgentData));
}
}
for (const extension of delta.removed) {
for (const providerDescriptor of extension.value) {
this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier });
this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name));
}
}
});
@@ -314,15 +344,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup);
function getParticipantKey(participant: IChatParticipantContribution): string {
return `${participant.extensionId.value}_${participant.name}`;
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
return `${extensionId.value}_${participantName}`;
}
export class ChatContributionService implements IChatContributionService {
declare _serviceBrand: undefined;
private _registeredProviders = new Map<string, IChatProviderContribution>();
private _registeredParticipants = new Map<string, IChatParticipantContribution>();
constructor(
) { }
@@ -339,19 +368,7 @@ export class ChatContributionService implements IChatContributionService {
this._registeredProviders.delete(providerId);
}
public registerChatParticipant(participant: IChatParticipantContribution): void {
this._registeredParticipants.set(getParticipantKey(participant), participant);
}
public deregisterChatParticipant(participant: IChatParticipantContribution): void {
this._registeredParticipants.delete(getParticipantKey(participant));
}
public get registeredProviders(): IChatProviderContribution[] {
return Array.from(this._registeredProviders.values());
}
public get registeredParticipants(): IChatParticipantContribution[] {
return Array.from(this._registeredParticipants.values());
}
}
@@ -360,7 +360,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
let progressMsg: string = '';
if (element.agent && !element.agent.isDefault) {
let usingMsg = chatAgentLeader + element.agent.id;
let usingMsg = chatAgentLeader + element.agent.name;
if (element.slashCommand) {
usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`;
}
@@ -11,7 +11,7 @@ import { Location } from 'vs/editor/common/languages';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label';
import { ILogService } from 'vs/platform/log/common/log';
import { ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { contentRefUrl } from '../common/annotations';
const variableRefUrl = 'http://_vscodedecoration_';
@@ -31,7 +31,9 @@ export class ChatMarkdownDecorationsRenderer {
} else {
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
|| undefined;
const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : '';
const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) :
part instanceof ChatRequestAgentPart ? part.agent.id :
'';
result += `[${part.text}](${variableRefUrl}?${title})`;
}
@@ -106,4 +108,3 @@ export class ChatMarkdownDecorationsRenderer {
}
}
}
@@ -145,7 +145,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
private parsedChatRequest: IParsedChatRequest | undefined;
get parsedInput() {
if (this.parsedChatRequest === undefined) {
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location);
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent });
}
return this.parsedChatRequest;
@@ -213,6 +213,15 @@ export class ChatWidget extends Disposable implements IChatWidget {
}));
}
private _lastSelectedAgent: IChatAgentData | undefined;
set lastSelectedAgent(agent: IChatAgentData | undefined) {
this._lastSelectedAgent = agent;
}
get lastSelectedAgent(): IChatAgentData | undefined {
return this._lastSelectedAgent;
}
get supportsFileReferences(): boolean {
return !!this.viewOptions.supportsFileReferences;
}
@@ -673,7 +682,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
'query' in opts ? opts.query :
`${opts.prefix} ${editorValue}`;
const isUserQuery = !opts || 'prefix' in opts;
const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, this.location);
const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, this.location, { selectedAgent: this._lastSelectedAgent });
if (result) {
const inputState = this.collectInputState();
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { Position } from 'vs/editor/common/core/position';
@@ -14,7 +15,8 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList }
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -37,8 +39,8 @@ const placeholderDecorationType = 'chat-session-detail';
const slashCommandTextDecorationType = 'chat-session-text';
const variableTextDecorationType = 'chat-variable-text';
function agentAndCommandToKey(agent: string, subcommand: string | undefined): string {
return subcommand ? `${agent}__${subcommand}` : agent;
function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string {
return subcommand ? `${agent.id}__${subcommand}` : agent.id;
}
class InputEditorDecorations extends Disposable {
@@ -70,7 +72,7 @@ class InputEditorDecorations extends Disposable {
this.updateInputEditorDecorations();
}));
this._register(this.widget.onDidSubmitAgent((e) => {
this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent.id, e.slashCommand?.name));
this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name));
}));
this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations()));
@@ -135,7 +137,7 @@ class InputEditorDecorations extends Disposable {
},
renderOptions: {
after: {
contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '',
contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '',
color: this.getPlaceholderColor()
}
}
@@ -172,14 +174,14 @@ class InputEditorDecorations extends Disposable {
const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart);
if (onlyAgentAndWhitespace) {
// Agent reference with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, undefined));
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder;
if (agentPart.agent.metadata.description && exactlyOneSpaceAfterPart(agentPart)) {
if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) {
placeholderDecoration = [{
range: getRangeForPlaceholder(agentPart),
renderOptions: {
after: {
contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.metadata.description,
contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description,
color: this.getPlaceholderColor(),
}
}
@@ -190,7 +192,7 @@ class InputEditorDecorations extends Disposable {
const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart);
if (onlyAgentCommandAndWhitespace) {
// Agent reference and subcommand with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name));
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder;
if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) {
placeholderDecoration = [{
@@ -209,9 +211,10 @@ class InputEditorDecorations extends Disposable {
const textDecorations: IDecorationOptions[] | undefined = [];
if (agentPart) {
textDecorations.push({ range: agentPart.editorRange });
const agentHover = `(${agentPart.agent.id}) ${agentPart.agent.description}`;
textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) });
if (agentSubcommandPart) {
textDecorations.push({ range: agentSubcommandPart.editorRange });
textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) });
}
}
@@ -246,9 +249,9 @@ class InputEditorSlashCommandMode extends Disposable {
private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) {
let value: string | undefined;
if (slashCommand && slashCommand.isSticky) {
value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `;
value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `;
} else if (agent.metadata.isSticky) {
value = `${chatAgentLeader}${agent.id} `;
value = `${chatAgentLeader}${agent.name} `;
}
if (value) {
@@ -349,13 +352,18 @@ class AgentCompletions extends Disposable {
.filter(a => a.locations.includes(widget.location));
return <CompletionList>{
suggestions: agents.map((c, i) => {
const withAt = `@${c.id}`;
suggestions: agents.map((a, i) => {
const withAt = `@${a.name}`;
const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id);
return <CompletionItem>{
label: withAt,
// Leading space is important because detail has no space at the start by design
label: isDupe ?
{ label: withAt, description: a.description, detail: ` (${a.id})` } :
withAt,
insertText: `${withAt} `,
detail: c.metadata.description,
detail: a.description,
range: new Range(1, 1, 1, 1),
command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] },
kind: CompletionItemKind.Text, // The icons are disabled here anyway
};
})
@@ -435,31 +443,37 @@ class AgentCompletions extends Disposable {
const justAgents: CompletionItem[] = agents
.filter(a => !a.isDefault)
.map(agent => {
const agentLabel = `${chatAgentLeader}${agent.id}`;
const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id);
const detail = agent.description;
const agentLabel = `${chatAgentLeader}${agent.name} (${agent.id})`;
return {
label: { label: agentLabel, description: agent.metadata.description },
filterText: `${chatSubcommandLeader}${agent.id}`,
label: isDupe ?
{ label: agentLabel, description: agent.description, detail: ` (${agent.id})` } :
agentLabel,
detail,
filterText: `${chatSubcommandLeader}${agent.name}`,
insertText: `${agentLabel} `,
range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text,
sortText: `${chatSubcommandLeader}${agent.id}`,
sortText: `${chatSubcommandLeader}${agent.name}`,
};
});
return {
suggestions: justAgents.concat(
agents.flatMap(agent => agent.slashCommands.map((c, i) => {
const agentLabel = `${chatAgentLeader}${agent.id}`;
const agentLabel = `${chatAgentLeader}${agent.name}`;
const withSlash = `${chatSubcommandLeader}${c.name}`;
return {
label: { label: withSlash, description: agentLabel },
filterText: `${chatSubcommandLeader}${agent.id}${c.name}`,
filterText: `${chatSubcommandLeader}${agent.name}${c.name}`,
commitCharacters: [' '],
insertText: `${agentLabel} ${withSlash} `,
detail: `(${agentLabel}) ${c.description ?? ''}`,
range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text, // The icons are disabled here anyway
sortText: `${chatSubcommandLeader}${agent.id}${c.name}`,
sortText: `${chatSubcommandLeader}${agent.name}${c.name}`,
} satisfies CompletionItem;
})))
};
@@ -469,6 +483,32 @@ class AgentCompletions extends Disposable {
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually);
interface AssignSelectedAgentActionArgs {
agent: IChatAgentData;
widget: IChatWidget;
}
class AssignSelectedAgentAction extends Action2 {
static readonly ID = 'workbench.action.chat.assignSelectedAgent';
constructor() {
super({
id: AssignSelectedAgentAction.ID,
title: '' // not displayed
});
}
async run(accessor: ServicesAccessor, ...args: any[]) {
const arg: AssignSelectedAgentActionArgs = args[0];
if (!arg || !arg.widget || !arg.agent) {
return;
}
arg.widget.lastSelectedAgent = arg.agent;
}
}
registerAction2(AssignSelectedAgentAction);
class BuiltinDynamicCompletions extends Disposable {
private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag
@@ -596,12 +636,14 @@ class ChatTokenDeleter extends Disposable {
const parser = this.instantiationService.createInstance(ChatRequestParser);
const inputValue = this.widget.inputEditor.getValue();
let previousInputValue: string | undefined;
let previousSelectedAgent: IChatAgentData | undefined;
// A simple heuristic to delete the previous token when the user presses backspace.
// The sophisticated way to do this would be to have a parse tree that can be updated incrementally.
this._register(this.widget.inputEditor.onDidChangeModelContent(e => {
if (!previousInputValue) {
previousInputValue = inputValue;
previousSelectedAgent = this.widget.lastSelectedAgent;
}
// Don't try to handle multicursor edits right now
@@ -609,7 +651,7 @@ class ChatTokenDeleter extends Disposable {
// If this was a simple delete, try to find out whether it was inside a token
if (!change.text && this.widget.viewModel) {
const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue);
const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, { selectedAgent: previousSelectedAgent });
// For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping
const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart);
@@ -629,6 +671,7 @@ class ChatTokenDeleter extends Disposable {
}
previousInputValue = this.widget.inputEditor.getValue();
previousSelectedAgent = this.widget.lastSelectedAgent;
}));
}
}
@@ -506,6 +506,7 @@
.quick-input-widget .interactive-session .interactive-input-part {
padding: 8px 6px 6px 6px;
margin: 0 3px;
}
.quick-input-widget .interactive-session .interactive-input-part .interactive-execute-toolbar {
@@ -3,19 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray, distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ProviderResult } from 'vs/editor/common/languages';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatContributionService, IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
@@ -47,6 +46,8 @@ export namespace ChatAgentLocation {
export interface IChatAgentData {
id: string;
name: string;
description?: string;
extensionId: ExtensionIdentifier;
/** The agent invoked when no agent is specified */
isDefault?: boolean;
@@ -79,7 +80,6 @@ export interface IChatRequesterInformation {
}
export interface IChatAgentMetadata {
description?: string;
helpTextPrefix?: string | IMarkdownString;
helpTextVariablesPrefix?: string | IMarkdownString;
helpTextPostfix?: string | IMarkdownString;
@@ -118,86 +118,102 @@ export interface IChatAgentResult {
export const IChatAgentService = createDecorator<IChatAgentService>('chatAgentService');
interface IChatAgentEntry {
data: IChatAgentData;
impl?: IChatAgentImplementation;
}
export interface IChatAgentService {
_serviceBrand: undefined;
/**
* undefined when an agent was removed IChatAgent
*/
readonly onDidChangeAgents: Event<IChatAgent | undefined>;
registerAgent(name: string, agent: IChatAgentImplementation): IDisposable;
registerAgent(id: string, data: IChatAgentData): IDisposable;
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable;
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable;
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>;
getAgents(): IChatAgentData[];
getRegisteredAgents(): Array<IChatAgentData>;
getActivatedAgents(): Array<IChatAgent>;
getAgent(id: string): IChatAgentData | undefined;
getAgents(): IChatAgentData[];
getActivatedAgents(): Array<IChatAgent>;
getAgentsByName(name: string): IChatAgentData[];
getDefaultAgent(): IChatAgent | undefined;
getSecondaryAgent(): IChatAgentData | undefined;
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void;
}
export class ChatAgentService extends Disposable implements IChatAgentService {
export class ChatAgentService implements IChatAgentService {
public static readonly AGENT_LEADER = '@';
declare _serviceBrand: undefined;
private readonly _agents = new Map<string, { data: IChatAgentData; impl?: IChatAgentImplementation }>();
private _agents: IChatAgentEntry[] = [];
private readonly _onDidChangeAgents = this._register(new Emitter<IChatAgent | undefined>());
private readonly _onDidChangeAgents = new Emitter<IChatAgent | undefined>();
readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event;
constructor(
@IChatContributionService private chatContributionService: IChatContributionService,
@IContextKeyService private contextKeyService: IContextKeyService,
) {
super();
}
@IContextKeyService private readonly contextKeyService: IContextKeyService
) { }
override dispose(): void {
super.dispose();
this._agents.clear();
}
registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable {
if (this._agents.has(name)) {
// TODO not keyed by name, dupes allowed between extensions
throw new Error(`Already registered an agent with id ${name}`);
registerAgent(id: string, data: IChatAgentData): IDisposable {
const existingAgent = this.getAgent(id);
if (existingAgent) {
throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
}
const data = this.getAgent(name);
if (!data) {
throw new Error(`Unknown agent: ${name}`);
const that = this;
const commands = data.slashCommands;
data = {
...data,
get slashCommands() {
return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when)));
}
};
const entry = { data };
this._agents.push(entry);
return toDisposable(() => {
this._agents = this._agents.filter(a => a !== entry);
this._onDidChangeAgents.fire(undefined);
});
}
registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable {
const entry = this._getAgentEntry(id);
if (!entry) {
throw new Error(`Unknown agent: ${JSON.stringify(id)}`);
}
const agent = { data: data, impl: agentImpl };
this._agents.set(name, agent);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl));
if (entry.impl) {
throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`);
}
entry.impl = agentImpl;
this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl));
return toDisposable(() => {
if (this._agents.delete(name)) {
this._onDidChangeAgents.fire(undefined);
}
entry.impl = undefined;
this._onDidChangeAgents.fire(undefined);
});
}
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable {
const agent = { data, impl: agentImpl };
this._agents.set(data.id, agent);
this._agents.push(agent);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl));
return toDisposable(() => {
if (this._agents.delete(data.id)) {
this._onDidChangeAgents.fire(undefined);
}
this._agents = this._agents.filter(a => a !== agent);
this._onDidChangeAgents.fire(undefined);
});
}
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void {
const agent = this._agents.get(id);
const agent = this._getAgentEntry(id);
if (!agent?.impl) {
throw new Error(`No activated agent with id ${id} registered`);
throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`);
}
agent.data.metadata = { ...agent.data.metadata, ...updateMetadata };
this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl));
@@ -213,35 +229,19 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data;
}
getRegisteredAgents(): Array<IChatAgentData> {
const that = this;
return this.chatContributionService.registeredParticipants.map(p => (
{
extensionId: p.extensionId,
id: p.name,
metadata: this._agents.has(p.name) ? this._agents.get(p.name)!.data.metadata : { description: p.description },
isDefault: p.isDefault,
defaultImplicitVariables: p.defaultImplicitVariables,
locations: isNonEmptyArray(p.locations) ? p.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel],
get slashCommands() {
const commands = p.commands ?? [];
return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when)));
}
} satisfies IChatAgentData));
private _getAgentEntry(id: string): IChatAgentEntry | undefined {
return this._agents.find(a => a.data.id === id);
}
getAgent(id: string): IChatAgentData | undefined {
return this._getAgentEntry(id)?.data;
}
/**
* Returns all agent datas that exist- static registered and dynamic ones.
*/
getAgents(): IChatAgentData[] {
const registeredAgents = this.getRegisteredAgents();
const dynamicAgents = Array.from(this._agents.values()).map(a => a.data);
const all = [
...registeredAgents,
...dynamicAgents
];
return distinct(all, a => a.id);
return this._agents.map(entry => entry.data);
}
getActivatedAgents(): IChatAgent[] {
@@ -250,12 +250,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
.map(a => new MergedChatAgent(a.data, a.impl!));
}
getAgent(id: string): IChatAgentData | undefined {
return this.getAgents().find(a => a.id === id);
getAgentsByName(name: string): IChatAgentData[] {
return this.getAgents().filter(a => a.name === name);
}
async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
const data = this._agents.get(id);
const data = this._getAgentEntry(id);
if (!data?.impl) {
throw new Error(`No activated agent with id ${id}`);
}
@@ -264,7 +264,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
}
async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> {
const data = this._agents.get(id);
const data = this._getAgentEntry(id);
if (!data?.impl) {
throw new Error(`No activated agent with id ${id}`);
}
@@ -284,6 +284,8 @@ export class MergedChatAgent implements IChatAgent {
) { }
get id(): string { return this.data.id; }
get name(): string { return this.data.name ?? ''; }
get description(): string { return this.data.description ?? ''; }
get extensionId(): ExtensionIdentifier { return this.data.extensionId; }
get isDefault(): boolean | undefined { return this.data.isDefault; }
get metadata(): IChatAgentMetadata { return this.data.metadata; }
@@ -19,10 +19,6 @@ export interface IChatContributionService {
registerChatProvider(provider: IChatProviderContribution): void;
deregisterChatProvider(providerId: string): void;
getViewIdForProvider(providerId: string): string;
readonly registeredParticipants: IChatParticipantContribution[];
registerChatParticipant(participant: IChatParticipantContribution): void;
deregisterChatParticipant(participant: IChatParticipantContribution): void;
}
export interface IRawChatProviderContribution {
@@ -44,6 +40,7 @@ export interface IRawChatCommandContribution {
export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook';
export interface IRawChatParticipantContribution {
id: string;
name: string;
description?: string;
isDefault?: boolean;
@@ -617,7 +617,7 @@ export class ChatModel extends Disposable implements IChatModel {
const request = new ChatRequestModel(this, parsedRequest, variableData);
if (raw.response || raw.result || (raw as any).responseErrorDetails) {
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
revive<ISerializableChatAgentData>(raw.agent) : undefined;
this.reviveSerializedAgent(raw.agent) : undefined;
// Port entries from old format
const result = 'responseErrorDetails' in raw ?
@@ -639,6 +639,16 @@ export class ChatModel extends Disposable implements IChatModel {
}
}
private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData {
const agent = 'name' in raw ?
raw :
{
...(raw as any),
name: (raw as any).id,
};
return revive(agent);
}
private getParsedRequestFromString(message: string): IParsedChatRequest {
// TODO These offsets won't be used, but chat replies need to go through the parser as well
const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];
@@ -729,7 +739,7 @@ export class ChatModel extends Disposable implements IChatModel {
} else if (progress.kind === 'usedContext' || progress.kind === 'reference') {
request.response.applyReference(progress);
} else if (progress.kind === 'agentDetection') {
const agent = this.chatAgentService.getAgent(progress.agentName);
const agent = this.chatAgentService.getAgent(progress.agentId);
if (agent) {
request.response.setAgent(agent, progress.command, progress.detected);
}
@@ -830,7 +840,7 @@ export class ChatModel extends Disposable implements IChatModel {
vote: r.response?.vote,
agent: r.response?.agent ?
// May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here.
{ id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault }
{ id: r.response.agent.id, name: r.response.agent.name, description: r.response.agent.description, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault }
: undefined,
slashCommand: r.response?.slashCommand,
usedContext: r.response?.usedContext,
@@ -75,7 +75,7 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { }
get text(): string {
return `${chatAgentLeader}${this.agent.id}`;
return `${chatAgentLeader}${this.agent.name}`;
}
get promptText(): string {
@@ -92,6 +92,8 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
editorRange: this.editorRange,
agent: {
id: this.agent.id,
name: this.agent.name,
description: this.agent.description,
metadata: this.agent.metadata
}
};
@@ -167,10 +169,19 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed
(part as ChatRequestVariablePart).variableArg
);
} else if (part.kind === ChatRequestAgentPart.Kind) {
let agent = (part as ChatRequestAgentPart).agent;
if (!('name' in agent)) {
// Port old format
agent = {
...(agent as any),
name: (agent as any).id
};
}
return new ChatRequestAgentPart(
new OffsetRange(part.range.start, part.range.endExclusive),
part.editorRange,
(part as ChatRequestAgentPart).agent
agent
);
} else if (part.kind === ChatRequestAgentSubcommandPart.Kind) {
return new ChatRequestAgentSubcommandPart(
@@ -6,10 +6,8 @@
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables';
@@ -17,19 +15,21 @@ const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command
export interface IChatParserContext {
/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */
selectedAgent?: IChatAgentData;
}
export class ChatRequestParser {
constructor(
@IChatAgentService private readonly agentService: IChatAgentService,
@IChatVariablesService private readonly variableService: IChatVariablesService,
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
@IChatService private readonly chatService: IChatService
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
) { }
parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel): IParsedChatRequest {
parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest {
const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
const model = this.chatService.getSession(sessionId)!;
// TODO@jrieken put location on IChatModel
let lineNumber = 1;
let column = 1;
@@ -41,9 +41,9 @@ export class ChatRequestParser {
if (char === chatVariableLeader) {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts);
} else if (char === chatAgentLeader) {
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location);
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);
} else if (char === chatSubcommandLeader) {
newPart = this.tryToParseSlashCommand(model, message.slice(i), message, i, new Position(lineNumber, column), parts);
newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts);
}
if (!newPart) {
@@ -90,17 +90,23 @@ export class ChatRequestParser {
};
}
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, location: ChatAgentLocation): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextVariableMatch = message.match(agentReg);
if (!nextVariableMatch) {
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextAgentMatch = message.match(agentReg);
if (!nextAgentMatch) {
return;
}
const [full, name] = nextVariableMatch;
const varRange = new OffsetRange(offset, offset + full.length);
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
const [full, name] = nextAgentMatch;
const agentRange = new OffsetRange(offset, offset + full.length);
const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
const agent = this.agentService.getAgent(name);
const agents = this.agentService.getAgentsByName(name);
// If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the
// context and we use that one. Otherwise just pick the first.
const agent = agents.length > 1 && context?.selectedAgent ?
context.selectedAgent :
agents[0];
if (!agent || !agent.locations.includes(location)) {
return;
}
@@ -122,7 +128,7 @@ export class ChatRequestParser {
return;
}
return new ChatRequestAgentPart(varRange, varEditorRange, agent);
return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);
}
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
@@ -143,7 +149,7 @@ export class ChatRequestParser {
return;
}
private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
const nextSlashMatch = remainingMessage.match(slashReg);
if (!nextSlashMatch) {
return;
@@ -15,6 +15,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
export interface IChat {
@@ -85,7 +86,7 @@ export interface IChatContentInlineReference {
}
export interface IChatAgentDetection {
agentName: string;
agentId: string;
command?: IChatAgentCommand;
detected?: boolean;
kind: 'agentDetection';
@@ -291,7 +292,7 @@ export interface IChatService {
/**
* Returns whether the request was accepted.
*/
sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation): Promise<IChatSendRequestData | undefined>;
sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation, parserContext?: IChatParserContext): Promise<IChatSendRequestData | undefined>;
removeRequest(sessionid: string, requestId: string): Promise<void>;
cancelCurrentRequestForSession(sessionId: string): void;
clearSession(sessionId: string): void;
@@ -25,7 +25,7 @@ import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, ICh
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { ChatRequestParser, IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
@@ -435,7 +435,7 @@ export class ChatService extends Disposable implements IChatService {
return this._startSession(data.providerId, data, CancellationToken.None);
}
async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, location: ChatAgentLocation = ChatAgentLocation.Panel): Promise<IChatSendRequestData | undefined> {
async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, location: ChatAgentLocation = ChatAgentLocation.Panel, parserContext?: IChatParserContext): Promise<IChatSendRequestData | undefined> {
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
if (!request.trim()) {
this.trace('sendRequest', 'Rejected empty message');
@@ -463,7 +463,7 @@ export class ChatService extends Disposable implements IChatService {
defaultAgent = this.chatAgentService.getActivatedAgents().find(a => a.locations.includes(location)) ?? defaultAgent;
}
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location);
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext);
const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
@@ -33,7 +33,7 @@ suite('ChatVariables', function () {
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatVariablesService, service);
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
});
test('ChatVariables - resolveVariables', async function () {
@@ -27,6 +27,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -27,6 +27,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
slashCommands: [
{
@@ -0,0 +1,96 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
message: {
text: "@ChatProviderWithUsedContext test request",
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 29
},
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
metadata: { }
}
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
]
},
variableData: { variables: [ ] },
response: [ ],
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
metadata: { },
slashCommands: [ ],
locations: [ 1 ],
isDefault: undefined
},
slashCommand: undefined,
usedContext: {
documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
],
kind: "usedContext"
},
contentReferences: [ ]
}
],
providerId: "testProvider"
}
@@ -0,0 +1,9 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [ ],
providerId: "testProvider"
}
@@ -0,0 +1,102 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
message: {
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 29
},
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
metadata: {
requester: { name: "test" },
fullName: "test"
}
}
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
],
text: "@ChatProviderWithUsedContext test request"
},
variableData: { variables: [ ] },
response: [ ],
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
metadata: {
requester: { name: "test" },
fullName: "test"
},
slashCommands: [ ],
locations: [ 1 ],
isDefault: undefined
},
slashCommand: undefined,
usedContext: {
documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
],
kind: "usedContext"
},
contentReferences: [ ]
}
],
providerId: "testProvider"
}
@@ -30,7 +30,7 @@ suite('ChatModel', () => {
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
});
test('Waits for initialization', async () => {
@@ -15,7 +15,7 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
suite('ChatRequestParser', () => {
@@ -31,7 +31,7 @@ suite('ChatRequestParser', () => {
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
varService = mockObject<IChatVariablesService>()({});
varService.getDynamicVariables.returns([]);
@@ -112,12 +112,12 @@ suite('ChatRequestParser', () => {
});
const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => {
return <IChatAgentData>{ id: 'agent', metadata: { description: '' }, slashCommands, locations: [ChatAgentLocation.Panel] };
return <IChatAgentData>{ id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [], metadata: { description: '' }, slashCommands, locations: [ChatAgentLocation.Panel] };
};
test('agent with subcommand after text', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -127,7 +127,7 @@ suite('ChatRequestParser', () => {
test('agents, subCommand', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -137,7 +137,7 @@ suite('ChatRequestParser', () => {
test('agent with question mark', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -147,7 +147,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand with leading whitespace', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -157,7 +157,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand after newline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -167,7 +167,7 @@ suite('ChatRequestParser', () => {
test('agent not first', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@@ -177,7 +177,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true);
@@ -189,7 +189,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline, part2', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true);
@@ -20,7 +20,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
@@ -28,11 +27,11 @@ import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChat
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService';
class SimpleTestProvider extends Disposable implements IChatProvider {
private static sessionId = 0;
@@ -57,6 +56,7 @@ class SimpleTestProvider extends Disposable implements IChatProvider {
const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';
const chatAgentWithUsedContext: IChatAgent = {
id: chatAgentWithUsedContextId,
name: chatAgentWithUsedContextId,
extensionId: nullExtensionDescription.identifier,
locations: [ChatAgentLocation.Panel],
metadata: {},
@@ -82,7 +82,7 @@ const chatAgentWithUsedContext: IChatAgent = {
},
};
suite('Chat', () => {
suite('ChatService', () => {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
let storageService: IStorageService;
@@ -104,13 +104,8 @@ suite('Chat', () => {
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatContributionService, new MockChatContributionService(
[
{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true },
{ extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId },
]));
chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService));
chatAgentService = instantiationService.createInstance(ChatAgentService);
instantiationService.stub(IChatAgentService, chatAgentService);
const agent = {
@@ -118,7 +113,9 @@ suite('Chat', () => {
return {};
},
} satisfies IChatAgentImplementation;
testDisposables.add(chatAgentService.registerAgent('testAgent', agent));
testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] }));
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] }));
testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent));
chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' });
});
@@ -209,7 +206,7 @@ suite('Chat', () => {
});
test('can serialize', async () => {
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext));
testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' });
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
@@ -230,7 +227,7 @@ suite('Chat', () => {
test('can deserialize', async () => {
let serializedChatData: ISerializableChatData;
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext));
testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
// create the first service, send request, get response, and serialize the state
{ // serapate block to not leak variables in outer scope
@@ -9,7 +9,6 @@ export class MockChatContributionService implements IChatContributionService {
_serviceBrand: undefined;
constructor(
public readonly registeredParticipants: IChatParticipantContribution[] = []
) { }
registeredProviders: IChatProviderContribution[] = [];
@@ -28,7 +28,10 @@ suite('VoiceChat', () => {
extensionId: ExtensionIdentifier = nullExtensionDescription.identifier;
locations: ChatAgentLocation[] = [ChatAgentLocation.Panel];
constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { }
public readonly name: string;
constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) {
this.name = id;
}
invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); }
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); }
metadata = {};
@@ -47,17 +50,18 @@ suite('VoiceChat', () => {
class TestChatAgentService implements IChatAgentService {
_serviceBrand: undefined;
readonly onDidChangeAgents = Event.None;
registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); }
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }
getRegisteredAgents(): Array<IChatAgent> { return agents; }
getActivatedAgents(): IChatAgent[] { return agents; }
getAgents(): IChatAgent[] { return agents; }
getAgent(id: string): IChatAgent | undefined { throw new Error(); }
getDefaultAgent(): IChatAgent | undefined { throw new Error(); }
getSecondaryAgent(): IChatAgent | undefined { throw new Error(); }
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); }
registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); }
getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }
getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); }
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); }
}
class TestSpeechService implements ISpeechService {
@@ -47,7 +47,7 @@ interface IOnEnterRule {
/**
* Serialized form of a language configuration
*/
interface ILanguageConfiguration {
export interface ILanguageConfiguration {
comments?: CommentRule;
brackets?: CharacterPair[];
autoClosingPairs?: Array<CharacterPair | IAutoClosingPairConditional>;
@@ -149,7 +149,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
}
}
private _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined {
private static _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined {
const source = configuration.comments;
if (typeof source === 'undefined') {
return undefined;
@@ -179,7 +179,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined {
private static _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined {
const source = configuration.brackets;
if (typeof source === 'undefined') {
return undefined;
@@ -203,7 +203,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined {
private static _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined {
const source = configuration.autoClosingPairs;
if (typeof source === 'undefined') {
return undefined;
@@ -249,7 +249,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined {
private static _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined {
const source = configuration.surroundingPairs;
if (typeof source === 'undefined') {
return undefined;
@@ -289,7 +289,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined {
private static _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined {
const source = configuration.colorizedBracketPairs;
if (typeof source === 'undefined') {
return undefined;
@@ -312,7 +312,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined {
private static _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined {
const source = configuration.onEnterRules;
if (typeof source === 'undefined') {
return undefined;
@@ -385,7 +385,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return result;
}
private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void {
public static extractValidConfig(languageId: string, configuration: ILanguageConfiguration): ExplicitLanguageConfiguration {
const comments = this._extractValidCommentRule(languageId, configuration);
const brackets = this._extractValidBrackets(languageId, configuration);
@@ -421,11 +421,15 @@ export class LanguageConfigurationFileHandler extends Disposable {
folding,
__electricCharacterSupport: undefined,
};
return richEditConfig;
}
private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void {
const richEditConfig = LanguageConfigurationFileHandler.extractValidConfig(languageId, configuration);
this._languageConfigurationService.register(languageId, richEditConfig, 50);
}
private _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined {
private static _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined {
if (typeof value === 'string') {
try {
return new RegExp(value, '');
@@ -454,7 +458,7 @@ export class LanguageConfigurationFileHandler extends Disposable {
return undefined;
}
private _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined {
private static _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined {
const increaseIndentPattern = this._parseRegex(languageId, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern);
if (!increaseIndentPattern) {
return undefined;
@@ -0,0 +1,312 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { getReindentEditOperations } from 'vs/editor/contrib/indentation/common/indentation';
import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint';
import { parse } from 'vs/base/common/json';
import { IRange } from 'vs/editor/common/core/range';
function getIRange(range: IRange): IRange {
return {
startLineNumber: range.startLineNumber,
startColumn: range.startColumn,
endLineNumber: range.endLineNumber,
endColumn: range.endColumn
};
}
suite('Auto-Reindentation - TypeScript/JavaScript', () => {
const languageId = 'ts-test';
const options: IRelaxedTextModelCreationOptions = {};
let disposables: DisposableStore;
let instantiationService: TestInstantiationService;
let languageConfigurationService: ILanguageConfigurationService;
setup(() => {
disposables = new DisposableStore();
instantiationService = createModelServices(disposables);
languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
const configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json');
const configString = fs.readFileSync(configPath).toString();
const config = <ILanguageConfiguration>parse(configString, []);
const configParsed = LanguageConfigurationFileHandler.extractValidConfig(languageId, config);
disposables.add(languageConfigurationService.register(languageId, configParsed));
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
// Test which can be ran to find cases of incorrect indentation...
test.skip('Find Cases of Incorrect Indentation', () => {
const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts');
const fileContents = fs.readFileSync(filePath).toString();
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
model.applyEdits(editOperations);
// save the files to disk
const initialFile = path.join('..', 'autoindent', 'initial.ts');
const finalFile = path.join('..', 'autoindent', 'final.ts');
fs.writeFileSync(initialFile, fileContents);
fs.writeFileSync(finalFile, model.getValue());
});
// Unit tests for increase and decrease indent patterns...
/**
* First increase indent and decrease indent patterns:
*
* - decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/
* - In (https://macromates.com/manual/en/appendix)
* Either we have white space before the closing bracket, or we have a multi line comment ending on that line followed by whitespaces
* This is followed by any character.
* Textmate decrease indent pattern is as follows: /^(.*\*\/)?\s*\}[;\s]*$/
* Presumably allowing multi line comments ending on that line implies that } is itself not part of a multi line comment
*
* - increaseIndentPattern: /^.*\{[^}"']*$/
* - In (https://macromates.com/manual/en/appendix)
* This regex means that we increase the indent when we have any characters followed by the opening brace, followed by characters
* except for closing brace }, double quotes " or single quote '.
* The } is checked in order to avoid the indentation in the following case `int arr[] = { 1, 2, 3 };`
* The double quote and single quote are checked in order to avoid the indentation in the following case: str = "foo {";
*/
test('Issue #25437', () => {
// issue: https://github.com/microsoft/vscode/issues/25437
// fix: https://github.com/microsoft/vscode/commit/8c82a6c6158574e098561c28d470711f1b484fc8
// explanation: var foo = `{`; should not increase indentation
// increaseIndentPattern: /^.*\{[^}"']*$/ -> /^.*\{[^}"'`]*$/
const fileContents = [
'const foo = `{`;',
' ',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
"startLineNumber": 2,
"startColumn": 1,
"endLineNumber": 2,
"endColumn": 5,
});
assert.deepStrictEqual(operation.text, '');
});
test('Enriching the hover', () => {
// issue: -
// fix: https://github.com/microsoft/vscode/commit/19ae0932c45b1096443a8c1335cf1e02eb99e16d
// explanation:
// - decrease indent on ) and ] also
// - increase indent on ( and [ also
// decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ -> /^(.*\*\/)?\s*[\}\]\)].*$/
// increaseIndentPattern: /^.*\{[^}"'`]*$/ -> /^.*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
let fileContents = [
'function foo(',
' bar: string',
' ){}',
].join('\n');
let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
let operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
"startLineNumber": 3,
"startColumn": 1,
"endLineNumber": 3,
"endColumn": 5,
});
assert.deepStrictEqual(operation.text, '');
fileContents = [
'function foo(',
'bar: string',
'){}',
].join('\n');
model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
"startLineNumber": 2,
"startColumn": 1,
"endLineNumber": 2,
"endColumn": 1,
});
assert.deepStrictEqual(operation.text, ' ');
});
test('Issue #86176', () => {
// issue: https://github.com/microsoft/vscode/issues/86176
// fix: https://github.com/microsoft/vscode/commit/d89e2e17a5d1ba37c99b1d3929eb6180a5bfc7a8
// explanation: When quotation marks are present on the first line of an if statement or for loop, following line should not be indented
// increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/
// explanation: after open brace, do not decrease indent if it is followed on the same line by "<whitespace characters> // <any characters>"
// todo@aiday-mar: should also apply for when it follows ( and [
const fileContents = [
`if () { // '`,
`x = 4`,
`}`
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
"startLineNumber": 2,
"startColumn": 1,
"endLineNumber": 2,
"endColumn": 1,
});
assert.deepStrictEqual(operation.text, ' ');
});
test('Issue #141816', () => {
// issue: https://github.com/microsoft/vscode/issues/141816
// fix: https://github.com/microsoft/vscode/pull/141997/files
// explanation: if (, [, {, is followed by a forward slash then assume we are in a regex pattern, and do not indent
// increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/
// -> Final current increase indent pattern
const fileContents = [
'const r = /{/;',
' ',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
"startLineNumber": 2,
"startColumn": 1,
"endLineNumber": 2,
"endColumn": 4,
});
assert.deepStrictEqual(operation.text, '');
});
test('Issue #29886', () => {
// issue: https://github.com/microsoft/vscode/issues/29886
// fix: https://github.com/microsoft/vscode/commit/7910b3d7bab8a721aae98dc05af0b5e1ea9d9782
// decreaseIndentPattern: /^(.*\*\/)?\s*[\}\]\)].*$/ -> /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/
// -> Final current decrease indent pattern
// explanation: Positive lookahead: (?= «pattern») matches if pattern matches what comes after the current location in the input string.
// Negative lookahead: (?! «pattern») matches if pattern does not match what comes after the current location in the input string
// The change proposed is to not decrease the indent if there is a multi-line comment ending on the same line before the closing parentheses
const fileContents = [
'function foo() {',
' bar(/* */)',
'};',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
// Failing tests inferred from the current regexes...
test.skip('Incorrect deindentation after `*/}` string', () => {
// explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent
// Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent
const fileContents = [
`const obj = {`,
` obj1: {`,
` brace : '*/}'`,
` }`,
`}`,
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
// Failing tests from issues...
test.skip('Issue #56275', () => {
// issue: https://github.com/microsoft/vscode/issues/56275
// explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent
// Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent
let fileContents = [
'function foo() {',
' var bar = (/b*/);',
'}',
].join('\n');
let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
fileContents = [
'function foo() {',
' var bar = "/b*/)";',
'}',
].join('\n');
model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test.skip('Issue #116843', () => {
// issue: https://github.com/microsoft/vscode/issues/116843
// related: https://github.com/microsoft/vscode/issues/43244
// explanation: When you have an arrow function, you don't have { or }, but you would expect indentation to still be done in that way
/*
Notes: Currently the reindent edit operations does not call the onEnter rules
The reindent should also call the onEnter rules to get the correct indentation
*/
const fileContents = [
'const add1 = (n) =>',
' n + 1;',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test.skip('Issue #185252', () => {
// issue: https://github.com/microsoft/vscode/issues/185252
// explanation: Reindenting the comment correctly
const fileContents = [
'/*',
' * This is a comment.',
' */',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
});
@@ -8,13 +8,17 @@ import { equals } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { mock } from 'vs/base/test/common/mock';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { IModelService } from 'vs/editor/common/services/model';
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
@@ -28,23 +32,19 @@ import { IViewDescriptorService } from 'vs/workbench/common/views';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
import { TestWorkerService } from './testWorkerService';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { Schemas } from 'vs/base/common/network';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
@@ -56,7 +56,6 @@ import { TestContextService, TestExtensionService } from 'vs/workbench/test/comm
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
@@ -133,8 +132,6 @@ suite('InteractiveChatController', function () {
[IChatService, new SyncDescriptor(ChatService)],
[IEditorWorkerService, new SyncDescriptor(TestWorkerService)],
[IContextKeyService, contextKeyService],
[IChatContributionService, new MockChatContributionService(
[{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])],
[IChatAgentService, new SyncDescriptor(ChatAgentService)],
[IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)],
[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],
@@ -168,7 +165,7 @@ suite('InteractiveChatController', function () {
}]
);
instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection));
instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection));
configurationService = instaService.get(IConfigurationService) as TestConfigurationService;
configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } });
@@ -186,7 +183,6 @@ suite('InteractiveChatController', function () {
},
} satisfies IChatAgentImplementation;
store.add(chatAgentService.registerAgent('testAgent', agent));
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null));
@@ -14,14 +14,18 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { changeCellToKind, computeCellLinesContents, copyCellRange, joinCellsWithSurrounds, joinSelectedCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellOutputViewModel, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { Range } from 'vs/editor/common/core/range';
import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController';
import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
//#region Move/Copy cells
const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp';
@@ -353,6 +357,7 @@ const COLLAPSE_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.collapseAllCellOutpu
const EXPAND_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.expandAllCellOutputs';
const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs';
const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling';
const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions';
registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction {
constructor() {
@@ -579,6 +584,45 @@ registerAction2(class ToggleCellOutputScrolling extends NotebookMultiCellAction
}
});
registerAction2(class ExpandAllCellOutputsAction extends NotebookCellAction {
constructor() {
super({
id: OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID,
title: localize2('notebookActions.cellFailureActions', "Show Cell Failure Actions"),
precondition: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()),
f1: true,
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()),
primary: KeyMod.CtrlCmd | KeyCode.Period,
weight: KeybindingWeight.WorkbenchContrib
}
});
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
if (context.cell instanceof CodeCellViewModel) {
const error = context.cell.cellErrorDetails;
if (error?.location) {
const location = Range.lift({
startLineNumber: error.location.startLineNumber + 1,
startColumn: error.location.startColumn + 1,
endLineNumber: error.location.endLineNumber + 1,
endColumn: error.location.endColumn + 1
});
context.notebookEditor.setCellEditorSelection(context.cell, Range.lift(location));
const editor = findTargetCellEditor(context, context.cell);
if (editor) {
const controller = CodeActionController.get(editor);
controller?.manualTriggerAtCurrentPosition(
localize('cellCommands.quickFix.noneMessage', "No code actions available"),
CodeActionTriggerSource.Default,
{ include: CodeActionKind.QuickFix });
}
}
}
}
});
//#endregion
function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) {
@@ -70,7 +70,9 @@ export class CellDiagnostics extends Disposable {
}
public clear() {
this.clearDiagnostics();
if (this.ErrorDetails) {
this.clearDiagnostics();
}
}
private clearDiagnostics() {
@@ -13,7 +13,7 @@ import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cell
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
export class CellContextKeyPart extends CellContentPart {
@@ -47,6 +47,7 @@ export class CellContextKeyManager extends Disposable {
private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>;
private cellResource!: IContextKey<string>;
private cellGeneratedByChat!: IContextKey<boolean>;
private cellHasErrorDiagnostics!: IContextKey<boolean>;
private markdownEditMode!: IContextKey<boolean>;
@@ -74,6 +75,7 @@ export class CellContextKeyManager extends Disposable {
this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService);
this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService);
this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService);
this.cellHasErrorDiagnostics = NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS.bindTo(this._contextKeyService);
if (element) {
this.updateForElement(element);
@@ -200,6 +202,10 @@ export class CellContextKeyManager extends Disposable {
this.cellRunState.set('idle');
this.cellExecuting.set(false);
}
if (this.element instanceof CodeCellViewModel) {
this.cellHasErrorDiagnostics.set(!!this.element.cellErrorDetails);
}
}
private updateForEditState() {
@@ -47,6 +47,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
private _outputCollection: number[] = [];
private readonly _cellDiagnostics: CellDiagnostics;
get cellErrorDetails() {
return this._cellDiagnostics.ErrorDetails;
}
private _outputsTop: PrefixSumComputer | null = null;
@@ -171,7 +174,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
if (outputLayoutChange) {
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs');
}
if (this._outputCollection.length === 0 && this._cellDiagnostics.ErrorDetails) {
if (this._outputCollection.length === 0) {
this._cellDiagnostics.clear();
}
dispose(removedOutputs);
@@ -433,6 +436,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
protected onDidChangeTextModelContent(): void {
if (this.getEditState() !== CellEditState.Editing) {
this._cellDiagnostics.clear();
this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');
this._onDidChangeState.fire({ contentChanged: true });
}
@@ -47,6 +47,7 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey<boolean>('noteboo
export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellOutputIsCollapsed', false);
export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellResource', '');
export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey<boolean>('notebookCellGenerateByChat', false);
export const NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS = new RawContextKey<boolean>('notebookCellHasErrorDiagnostics', false);
// Kernels
export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined);
@@ -254,7 +254,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<ITextSearch
picks.push({
label,
type: 'separator',
tooltip: description,
description,
buttons: [{
iconClass: ThemeIcon.asClassName(searchOpenInFileIcon),
tooltip: localize('QuickSearchOpenInFile', "Open File")
@@ -12,8 +12,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatAgentRequest, IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatUserAction, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
@@ -81,7 +81,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr
readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store);
readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store);
private _terminalAgentId = 'terminal';
private _terminalAgentName = 'terminal';
private _terminalAgentId: string | undefined;
private _model: MutableDisposable<ChatModel> = this._register(new MutableDisposable());
@@ -97,7 +98,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr
@IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IChatService private readonly _chatService: IChatService,
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService,
) {
super();
@@ -113,14 +114,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr
return;
}
if (!this._chatAgentService.getAgent(this._terminalAgentId)) {
this._register(this._chatAgentService.onDidChangeAgents(() => {
if (this._chatAgentService.getAgent(this._terminalAgentId)) {
this._terminalAgentRegisteredContextKey.set(true);
}
}));
} else {
this._terminalAgentRegisteredContextKey.set(true);
if (!this.initTerminalAgent()) {
this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent()));
}
this._register(this._chatCodeBlockContextProviderService.registerProvider({
getCodeBlockContext: (editor) => {
@@ -141,6 +136,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr
}, 'terminal'));
}
private initTerminalAgent(): boolean {
const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0];
if (terminalAgent) {
this._terminalAgentId = terminalAgent.id;
this._terminalAgentRegisteredContextKey.set(true);
return true;
}
return false;
}
xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) {
return;
@@ -286,13 +292,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr
const requestProps: IChatAgentRequest = {
sessionId: model.sessionId,
requestId: this._currentRequest!.id,
agentId: this._terminalAgentId,
agentId: this._terminalAgentId!,
message: this._lastInput,
variables: { variables: [] },
location: ChatAgentLocation.Terminal
};
try {
const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken);
const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken);
this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined);
this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined);
this._chatWidget?.value.inlineChatWidget.updateProgress(true);
+11 -12
View File
@@ -21,9 +21,9 @@ declare module 'vscode' {
readonly prompt: string;
/**
* The name of the chat participant and contributing extension to which this request was directed.
* The id of the chat participant and contributing extension to which this request was directed.
*/
readonly participant: { readonly extensionId: string; readonly name: string };
readonly participant: string;
/**
* The name of the {@link ChatCommand command} that was selected for this request.
@@ -35,7 +35,7 @@ declare module 'vscode' {
*/
readonly variables: ChatResolvedVariable[];
private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: { extensionId: string; name: string });
private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: string);
}
/**
@@ -54,16 +54,16 @@ declare module 'vscode' {
readonly result: ChatResult;
/**
* The name of the chat participant and contributing extension that this response came from.
* The id of the chat participant and contributing extension that this response came from.
*/
readonly participant: { readonly extensionId: string; readonly name: string };
readonly participant: string;
/**
* The name of the command that this response came from.
*/
readonly command?: string;
private constructor(response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>, result: ChatResult, participant: { extensionId: string; name: string });
private constructor(response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>, result: ChatResult, participant: string);
}
export interface ChatContext {
@@ -158,7 +158,7 @@ declare module 'vscode' {
label?: string;
/**
* By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant.
* By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID.
* Followups can only invoke a participant that was contributed by the same extension.
*/
participant?: string;
@@ -192,9 +192,9 @@ declare module 'vscode' {
*/
export interface ChatParticipant {
/**
* The short name by which this participant is referred to in the UI, e.g `workspace`.
* A unique ID for this participant.
*/
readonly name: string;
readonly id: string;
/**
* Icon for the participant shown in UI.
@@ -450,12 +450,11 @@ declare module 'vscode' {
/**
* Create a new {@link ChatParticipant chat participant} instance.
*
* @param name Short name by which the participant is referred to in the UI. The name must be unique for the extension
* contributing the participant but can collide with names from other extensions.
* @param id A unique identifier for the participant.
* @param handler A request handler for the participant.
* @returns A new chat participant
*/
export function createChatParticipant(name: string, handler: ChatRequestHandler): ChatParticipant;
export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant;
}
/**
@@ -173,7 +173,9 @@ declare module 'vscode' {
/**
* Create a chat participant with the extended progress type
*/
export function createChatParticipant(name: string, handler: ChatExtendedRequestHandler): ChatParticipant;
export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant;
export function createDynamicChatParticipant(id: string, name: string, description: string, handler: ChatExtendedRequestHandler): ChatParticipant;
}
/*
@@ -280,12 +282,4 @@ declare module 'vscode' {
*/
resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult<ChatVariableValue[]>;
}
export interface ChatParticipant {
/**
* A human-readable description explaining what this participant does.
* Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well.
*/
description?: string;
}
}