mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-14 16:04:09 +01:00
Merge branch 'main' into joh/chatWidget
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
];
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
) { }
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 () {
|
||||
|
||||
+6
@@ -27,6 +27,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -27,6 +27,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -13,6 +13,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -13,6 +13,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -13,6 +13,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -13,6 +13,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+6
@@ -13,6 +13,12 @@
|
||||
},
|
||||
agent: {
|
||||
id: "agent",
|
||||
name: "agent",
|
||||
extensionId: {
|
||||
value: "nullExtensionDescription",
|
||||
_lower: "nullextensiondescription"
|
||||
},
|
||||
locations: [ ],
|
||||
metadata: { description: "" },
|
||||
slashCommands: [
|
||||
{
|
||||
|
||||
+96
@@ -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"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
requesterUsername: "test",
|
||||
requesterAvatarIconUri: undefined,
|
||||
responderUsername: "test",
|
||||
responderAvatarIconUri: undefined,
|
||||
welcomeMessage: undefined,
|
||||
requests: [ ],
|
||||
providerId: "testProvider"
|
||||
}
|
||||
+102
@@ -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 {
|
||||
|
||||
+14
-10
@@ -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) {
|
||||
|
||||
+3
-1
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user