From 48f447cea7f52c9f9e3c1a7edd261d7cafd8613f Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 13 Mar 2024 15:00:45 +0100 Subject: [PATCH 01/47] Scaffold auto indentation testing --- .../indentation/browser/indentation.ts | 118 +---------------- .../{browser => common}/indentUtils.ts | 0 .../contrib/indentation/common/indentation.ts | 124 ++++++++++++++++++ .../browser/moveLinesCommand.ts | 2 +- .../api/browser/extensionHost.contribution.ts | 2 +- .../languageConfigurationExtensionPoint.ts | 22 ++-- .../codeEditor/test/node/autoindent.test.ts | 52 ++++++++ 7 files changed, 194 insertions(+), 126 deletions(-) rename src/vs/editor/contrib/indentation/{browser => common}/indentUtils.ts (100%) create mode 100644 src/vs/editor/contrib/indentation/common/indentation.ts rename src/vs/workbench/contrib/codeEditor/{browser => common}/languageConfigurationExtensionPoint.ts (96%) create mode 100644 src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index a14b7d6d5ae..1db0b5adb34 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -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'; diff --git a/src/vs/editor/contrib/indentation/browser/indentUtils.ts b/src/vs/editor/contrib/indentation/common/indentUtils.ts similarity index 100% rename from src/vs/editor/contrib/indentation/browser/indentUtils.ts rename to src/vs/editor/contrib/indentation/common/indentUtils.ts diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts new file mode 100644 index 00000000000..760a14919fb --- /dev/null +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -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; +} diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 8db880a15ad..68614a2f432 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -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'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 2b59166259d..69b049dfd60 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -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 diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts similarity index 96% rename from src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts rename to src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts index f602d051103..0d241fc0da9 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts @@ -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; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts new file mode 100644 index 00000000000..b22aaf9f455 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as arrays from 'vs/base/common/arrays'; +import * as arraysFind from 'vs/base/common/arraysFind'; +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 { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; + +suite('Manual Auto Indentation Evaluation', () => { + const languageId = 'ts-my-test-language'; + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createModelServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + // TODO: read the language-configuration.json file + const config = LanguageConfigurationFileHandler.extractValidConfig(languageId, JSON.parse("{}")); + disposables.add(languageConfigurationService.register(languageId, config)); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test.skip('TypeScript', () => { + // TODO: use fs to read a test file + const fileContents = "class X {\nconstructor() {\nconsole.log('Hello, world!');\n}\n}"; + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, {/*tabsize*/ })); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + model.applyEdits(editOperations); + + console.log( + editOperations + ); + }); + + // unit tests... + +}); From 00e30a5387814d3d167918fec7f8005759f7d708 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 13 Mar 2024 16:01:41 +0100 Subject: [PATCH 02/47] adding changes --- .../test/browser/indentation.test.ts | 2 ++ .../codeEditor/test/node/autoindent.test.ts | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 516966c9477..9ba11c867cd 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -20,6 +20,8 @@ import { javascriptIndentationRules } from 'vs/editor/test/common/modes/supports import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; +// TODO@aiday-mar what is the link with autoindent.test.ts + 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); } diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index b22aaf9f455..9be0836dadd 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -6,26 +6,31 @@ import * as assert from 'assert'; import * as arrays from 'vs/base/common/arrays'; import * as arraysFind from 'vs/base/common/arraysFind'; +import * as fs from 'fs'; +import * as path from 'path'; 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 { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; +import { FileAccess } from 'vs/base/common/network'; suite('Manual Auto Indentation Evaluation', () => { - const languageId = 'ts-my-test-language'; + const languageId = 'ts-test'; let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageConfigurationService: ILanguageConfigurationService; + let input: string; setup(() => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - // TODO: read the language-configuration.json file - const config = LanguageConfigurationFileHandler.extractValidConfig(languageId, JSON.parse("{}")); + const fsPath = FileAccess.asFileUri('extensions/typescript-basics/language-configuration.json').fsPath; + input = fs.readFileSync(fsPath).toString(); + const config = LanguageConfigurationFileHandler.extractValidConfig(languageId, JSON.parse(input)); disposables.add(languageConfigurationService.register(languageId, config)); }); @@ -35,6 +40,10 @@ suite('Manual Auto Indentation Evaluation', () => { ensureNoDisposablesAreLeakedInTestSuite(); + test('Test', () => { + console.log('input : ', input); + }); + test.skip('TypeScript', () => { // TODO: use fs to read a test file const fileContents = "class X {\nconstructor() {\nconsole.log('Hello, world!');\n}\n}"; From 3e72f946c7f84e9e1d427e800f89be47b616d830 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 13 Mar 2024 18:00:20 +0100 Subject: [PATCH 03/47] adding code to read file and indent it --- .../languageConfigurationExtensionPoint.ts | 2 +- .../codeEditor/test/node/autoindent.test.ts | 41 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts index 0d241fc0da9..e3d1fcd1064 100644 --- a/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts @@ -47,7 +47,7 @@ interface IOnEnterRule { /** * Serialized form of a language configuration */ -interface ILanguageConfiguration { +export interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; autoClosingPairs?: Array; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 9be0836dadd..f34cf90daca 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -3,35 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import * as arrays from 'vs/base/common/arrays'; -import * as arraysFind from 'vs/base/common/arraysFind'; import * as fs from 'fs'; import * as path from 'path'; 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 { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; -import { FileAccess } from 'vs/base/common/network'; +import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; +import { parse } from 'vs/base/common/json'; suite('Manual Auto Indentation Evaluation', () => { + const languageId = 'ts-test'; let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageConfigurationService: ILanguageConfigurationService; - let input: string; setup(() => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - const fsPath = FileAccess.asFileUri('extensions/typescript-basics/language-configuration.json').fsPath; - input = fs.readFileSync(fsPath).toString(); - const config = LanguageConfigurationFileHandler.extractValidConfig(languageId, JSON.parse(input)); - disposables.add(languageConfigurationService.register(languageId, config)); + const configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json'); + const configString = fs.readFileSync(configPath).toString(); + const config = parse(configString, []); + const configParsed = LanguageConfigurationFileHandler.extractValidConfig(languageId, config); + disposables.add(languageConfigurationService.register(languageId, configParsed)); }); teardown(() => { @@ -40,20 +38,21 @@ suite('Manual Auto Indentation Evaluation', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('Test', () => { - console.log('input : ', input); - }); + test('TypeScript', () => { - test.skip('TypeScript', () => { - // TODO: use fs to read a test file - const fileContents = "class X {\nconstructor() {\nconsole.log('Hello, world!');\n}\n}"; - const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, {/*tabsize*/ })); + const options: IRelaxedTextModelCreationOptions = {}; + 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); - console.log( - 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... From 1af831275c879c45e434ee3349f443383347955b Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 14 Mar 2024 09:22:08 +0100 Subject: [PATCH 04/47] adding unit tests from the blames --- .../codeEditor/test/node/autoindent.test.ts | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index f34cf90daca..93764b56560 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -5,6 +5,7 @@ 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'; @@ -17,6 +18,7 @@ import { parse } from 'vs/base/common/json'; suite('Manual Auto Indentation Evaluation', () => { const languageId = 'ts-test'; + const options: IRelaxedTextModelCreationOptions = {}; let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageConfigurationService: ILanguageConfigurationService; @@ -38,9 +40,9 @@ suite('Manual Auto Indentation Evaluation', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('TypeScript', () => { + // Test which can be ran to find cases of incorrect indentation + test('Find Cases of Incorrect Indentation', () => { - const options: IRelaxedTextModelCreationOptions = {}; const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts'); const fileContents = fs.readFileSync(filePath).toString(); @@ -55,6 +57,61 @@ suite('Manual Auto Indentation Evaluation', () => { fs.writeFileSync(finalFile, model.getValue()); }); - // unit tests... + // Unit tests + test('Issue #56275', () => { + + 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.deepEqual(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.deepEqual(editOperations.length, 0); + }); + + test('Issue #141816', () => { + + const fileContents = [ + 'const r = /{/;', + '', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepEqual(editOperations.length, 0); + }); + + test('Issue #114236', () => { + + let fileContents = [ + `/*-----`, + ` * line 1`, + ` *-----*/`, + ``, + `x` + ].join('\n'); + let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepEqual(editOperations.length, 0); + + fileContents = [ + `/*-----`, + ` * line 1`, + ` * line 2`, + ` * x` + ].join('\n'); + model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepEqual(editOperations.length, 0); + }); }); From b24ea8ebf024d0e89a9070a753de050e6603866e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 15 Mar 2024 14:32:27 +0100 Subject: [PATCH 05/47] updating the tests with history of the current regex --- .../codeEditor/test/node/autoindent.test.ts | 210 ++++++++++++++++-- 1 file changed, 188 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 93764b56560..1709079b0bd 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -14,6 +14,7 @@ import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextM 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'; suite('Manual Auto Indentation Evaluation', () => { @@ -40,7 +41,7 @@ suite('Manual Auto Indentation Evaluation', () => { ensureNoDisposablesAreLeakedInTestSuite(); - // Test which can be ran to find cases of incorrect indentation + // Test which can be ran to find cases of incorrect indentation... test('Find Cases of Incorrect Indentation', () => { const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts'); @@ -57,57 +58,213 @@ suite('Manual Auto Indentation Evaluation', () => { fs.writeFileSync(finalFile, model.getValue()); }); - // Unit tests - test('Issue #56275', () => { + // 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() {', - ' var bar = (/b*/);', - '}', + 'function foo(', + ' bar: string', + ' ){}', ].join('\n'); let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + 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() {', - ' var bar = "/b*/)";', - '}', + 'function foo(', + 'bar: string', + '){}', ].join('\n'); model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + 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 " // " + // 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.deepEqual(editOperations.length, 0); }); - test('Issue #114236', () => { + // Failing tests inferred from the current regexes... + + test('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 + + // todo@aiday-mar: would it make more sense to analyze the document from top to bottom to find matching [, {, (, in order to decide when to indent + // based on the work done by Henning + + 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()); + console.log('editOperations : ', JSON.stringify(editOperations)); + assert.deepEqual(editOperations.length, 0); + }); + + // Failing tests from issues... + + test('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 = [ - `/*-----`, - ` * line 1`, - ` *-----*/`, - ``, - `x` + '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.deepEqual(editOperations.length, 0); fileContents = [ - `/*-----`, - ` * line 1`, - ` * line 2`, - ` * x` + 'function foo() {', + ' var bar = "/b*/)";', + '}', ].join('\n'); model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); @@ -115,3 +272,12 @@ suite('Manual Auto Indentation Evaluation', () => { }); }); + +function getIRange(range: IRange): IRange { + return { + startLineNumber: range.startLineNumber, + startColumn: range.startColumn, + endLineNumber: range.endLineNumber, + endColumn: range.endColumn + }; +} From 668e8aa1ba4e03daa64c4a877993e0ae67e42469 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 15 Mar 2024 17:04:30 +0100 Subject: [PATCH 06/47] separating suites by language --- .../test/browser/indentation.test.ts | 658 +++++++++--------- .../codeEditor/test/node/autoindent.test.ts | 2 +- 2 files changed, 320 insertions(+), 340 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 9ba11c867cd..731f1edbf69 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -18,9 +18,7 @@ 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'; - -// TODO@aiday-mar what is the link with autoindent.test.ts +import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; 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); @@ -30,252 +28,284 @@ 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', () => { +suite('Indentation - TypeScript/Javascript', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('single tabs only at start of line', function () { - testIndentationToSpacesCommand( - [ - 'first', - 'second line', - 'third line', - '\tfourth line', - '\tfifth' - ], - new Selection(2, 3, 2, 3), - 4, - [ - 'first', - 'second line', - 'third line', - ' fourth line', - ' fifth' - ], - new Selection(2, 3, 2, 3) - ); - }); - - test('multiple tabs at start of line', function () { - testIndentationToSpacesCommand( - [ - '\t\tfirst', - '\tsecond line', - '\t\t\t third line', - 'fourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5), - 3, - [ - ' first', - ' second line', - ' third line', - 'fourth line', - 'fifth' - ], - new Selection(1, 9, 1, 9) - ); - }); - - test('multiple tabs', function () { - testIndentationToSpacesCommand( - [ - '\t\tfirst\t', - '\tsecond \t line \t', - '\t\t\t third line', - ' \tfourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5), - 2, - [ - ' first\t', - ' second \t line \t', - ' third line', - ' fourth line', - 'fifth' - ], - new Selection(1, 7, 1, 7) - ); - }); - - test('empty lines', function () { - testIndentationToSpacesCommand( - [ - '\t\t\t', - '\t', - '\t\t' - ], - new Selection(1, 4, 1, 4), - 2, - [ - ' ', - ' ', - ' ' - ], - new Selection(1, 4, 1, 4) - ); - }); -}); - -suite('Editor Contrib - Indentation to Tabs', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('spaces only at start of line', function () { - testIndentationToTabsCommand( - [ - ' first', - 'second line', - ' third line', - 'fourth line', - 'fifth' - ], - new Selection(2, 3, 2, 3), - 4, - [ - '\tfirst', - 'second line', - '\tthird line', - 'fourth line', - 'fifth' - ], - new Selection(2, 3, 2, 3) - ); - }); - - test('multiple spaces at start of line', function () { - testIndentationToTabsCommand( - [ - 'first', - ' second line', - ' third line', - 'fourth line', - ' fifth' - ], - new Selection(1, 5, 1, 5), - 3, - [ - 'first', - '\tsecond line', - '\t\t\t third line', - 'fourth line', - '\t fifth' - ], - new Selection(1, 5, 1, 5) - ); - }); - - test('multiple spaces', function () { - testIndentationToTabsCommand( - [ - ' first ', - ' second line \t', - ' third line', - ' fourth line', - 'fifth' - ], - new Selection(1, 8, 1, 8), - 2, - [ - '\t\t\tfirst ', - '\tsecond line \t', - '\t\t\t third line', - '\t fourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5) - ); - }); - - test('issue #45996', function () { - testIndentationToSpacesCommand( - [ - '\tabc', - ], - new Selection(1, 3, 1, 3), - 4, - [ - ' abc', - ], - new Selection(1, 6, 1, 6) - ); - }); -}); - -suite('Editor Contrib - Auto Indent On Paste', () => { + const languageId = 'ts-test'; let disposables: DisposableStore; setup(() => { disposables = new DisposableStore(); - }); - - teardown(() => { - disposables.dispose(); - }); - - 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); + const instantiationService = createModelServices(disposables); + 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); } - })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] + 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 + })); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('Change Indentation to Spaces', () => { + + test('single tabs only at start of line', function () { + testIndentationToSpacesCommand( + [ + 'first', + 'second line', + 'third line', + '\tfourth line', + '\tfifth' ], - comments: { - lineComment: '//', - blockComment: ['/*', '*/'] - }, - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); + new Selection(2, 3, 2, 3), + 4, + [ + 'first', + 'second line', + 'third line', + ' fourth line', + ' fifth' + ], + new Selection(2, 3, 2, 3) + ); + }); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); - const pasteText = [ - '/**', - ' * JSDoc', - ' */', - 'function a() {}' - ].join('\n'); + test('multiple tabs at start of line', function () { + testIndentationToSpacesCommand( + [ + '\t\tfirst', + '\tsecond line', + '\t\t\t third line', + 'fourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5), + 3, + [ + ' first', + ' second line', + ' third line', + 'fourth line', + 'fifth' + ], + new Selection(1, 9, 1, 9) + ); + }); - viewModel.paste(pasteText, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); - assert.strictEqual(model.getValue(), pasteText); + test('multiple tabs', function () { + testIndentationToSpacesCommand( + [ + '\t\tfirst\t', + '\tsecond \t line \t', + '\t\t\t third line', + ' \tfourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5), + 2, + [ + ' first\t', + ' second \t line \t', + ' third line', + ' fourth line', + 'fifth' + ], + new Selection(1, 7, 1, 7) + ); + }); + + test('empty lines', function () { + testIndentationToSpacesCommand( + [ + '\t\t\t', + '\t', + '\t\t' + ], + new Selection(1, 4, 1, 4), + 2, + [ + ' ', + ' ', + ' ' + ], + new Selection(1, 4, 1, 4) + ); + }); + }); + + suite('Change Indentation to Tabs', () => { + + test('spaces only at start of line', function () { + testIndentationToTabsCommand( + [ + ' first', + 'second line', + ' third line', + 'fourth line', + 'fifth' + ], + new Selection(2, 3, 2, 3), + 4, + [ + '\tfirst', + 'second line', + '\tthird line', + 'fourth line', + 'fifth' + ], + new Selection(2, 3, 2, 3) + ); + }); + + test('multiple spaces at start of line', function () { + testIndentationToTabsCommand( + [ + 'first', + ' second line', + ' third line', + 'fourth line', + ' fifth' + ], + new Selection(1, 5, 1, 5), + 3, + [ + 'first', + '\tsecond line', + '\t\t\t third line', + 'fourth line', + '\t fifth' + ], + new Selection(1, 5, 1, 5) + ); + }); + + test('multiple spaces', function () { + testIndentationToTabsCommand( + [ + ' first ', + ' second line \t', + ' third line', + ' fourth line', + 'fifth' + ], + new Selection(1, 8, 1, 8), + 2, + [ + '\t\t\tfirst ', + '\tsecond line \t', + '\t\t\t third line', + '\t fourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5) + ); + }); + + test('issue #45996', function () { + testIndentationToSpacesCommand( + [ + '\tabc', + ], + new Selection(1, 3, 1, 3), + 4, + [ + ' abc', + ], + new Selection(1, 6, 1, 6) + ); + }); + }); + + suite('Auto Indent On Paste', () => { + + 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) => { + + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + const pasteText = [ + '/**', + ' * JSDoc', + ' */', + 'function a() {}' + ].join('\n'); + + viewModel.paste(pasteText, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); + assert.strictEqual(model.getValue(), pasteText); + }); + }); + }); + + suite('Keep Indent On Paste', () => { + + 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 autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + const pasteText = [ + '', + 'export type IncludeReference =', + ' | BaseReference', + ' | SelfReference', + ' | RelativeReference;', + '', + 'export const enum IncludeReferenceKind {', + ' Base,', + ' Self,', + ' RelativeReference,', + '}' + ].join('\n'); + + viewModel.paste(pasteText, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); + assert.strictEqual(model.getValue(), pasteText); + }); }); }); }); -suite('Editor Contrib - Keep Indent On Paste', () => { +suite('Indentation - Ruby', () => { let disposables: DisposableStore; setup(() => { @@ -288,111 +318,61 @@ suite('Editor Contrib - Keep Indent On Paste', () => { 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 - })); + suite('Indent On Type', () => { - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); - const pasteText = [ - '', - 'export type IncludeReference =', - ' | BaseReference', - ' | SelfReference', - ' | RelativeReference;', - '', - 'export const enum IncludeReferenceKind {', - ' Base,', - ' Self,', - ' RelativeReference,', - '}' - ].join('\n'); + test('issue #198350: in or when incorrectly match non keywords for Ruby', () => { + const languageId = "ruby"; + const model = createTextModel("", languageId, {}); + disposables.add(model); - viewModel.paste(pasteText, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); - assert.strictEqual(model.getValue(), pasteText); - }); - }); -}); - -suite('Editor Contrib - Auto Dedent On Type', () => { - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - }); - - teardown(() => { - disposables.dispose(); - }); - - 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(); - - 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"); - viewModel.type(" ", 'keyboard'); - assert.strictEqual(model.getValue(), "def foo\nin "); - - viewModel.model.setValue(""); - viewModel.type(" # in"); - assert.strictEqual(model.getValue(), " # in"); - viewModel.type(" ", 'keyboard'); - assert.strictEqual(model.getValue(), " # in "); - improvedLanguageModel.dispose(); + 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(); + + 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"); + viewModel.type(" ", 'keyboard'); + assert.strictEqual(model.getValue(), "def foo\nin "); + + viewModel.model.setValue(""); + viewModel.type(" # in"); + assert.strictEqual(model.getValue(), " # in"); + viewModel.type(" ", 'keyboard'); + assert.strictEqual(model.getValue(), " # in "); + improvedLanguageModel.dispose(); + }); }); }); }); diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 1709079b0bd..132e120c755 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -16,7 +16,7 @@ import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/wor import { parse } from 'vs/base/common/json'; import { IRange } from 'vs/editor/common/core/range'; -suite('Manual Auto Indentation Evaluation', () => { +suite('Auto-Reindentation - TypeScript/JavaScript', () => { const languageId = 'ts-test'; const options: IRelaxedTextModelCreationOptions = {}; From 039be97cb05997e5f1a94b6201f70770b6a97ea3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 15 Mar 2024 18:35:01 +0100 Subject: [PATCH 07/47] look at the registry of the tokenization, why is there an error --- .../indentation/browser/indentation.ts | 20 ++ .../test/browser/indentation.test.ts | 206 +++++++++++------- 2 files changed, 149 insertions(+), 77 deletions(-) diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 1db0b5adb34..00818bd978e 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -379,21 +379,27 @@ export class AutoIndentOnPaste implements IEditorContribution { this.callOnModel.add(this.editor.onDidPaste(({ range }) => { this.trigger(range); + console.log('this.editor.getModel()?.getValue(); : ', this.editor.getModel()?.getValue()); })); } public trigger(range: Range): void { + console.log('trigger : ', JSON.stringify(range)); + const selections = this.editor.getSelections(); if (selections === null || selections.length > 1) { + console.log('return 1'); return; } const model = this.editor.getModel(); if (!model) { + console.log('return 2'); return; } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { + console.log('return 3'); return; } const autoIndent = this.editor.getOption(EditorOption.autoIndent); @@ -420,10 +426,12 @@ export class AutoIndentOnPaste implements IEditorContribution { } if (startLineNumber > range.endLineNumber) { + console.log('return 4'); return; } let firstLineText = model.getLineContent(startLineNumber); + console.log('firstLineText : ', firstLineText); if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) { const indentOfFirstLine = getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter, this._languageConfigurationService); @@ -432,6 +440,9 @@ export class AutoIndentOnPaste implements IEditorContribution { const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize); const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize); + console.log('newSpaceCnt : ', newSpaceCnt); + console.log('oldSpaceCnt : ', oldSpaceCnt); + if (newSpaceCnt !== oldSpaceCnt) { const newIndent = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces); textEdits.push({ @@ -447,6 +458,7 @@ export class AutoIndentOnPaste implements IEditorContribution { // after pasting, the indentation of the first line is already correct // the first line doesn't match any indentation rule // then no-op. + console.log('return 5'); return; } } @@ -486,6 +498,7 @@ export class AutoIndentOnPaste implements IEditorContribution { } }; const indentOfSecondLine = getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter, this._languageConfigurationService); + if (indentOfSecondLine !== null) { const newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize); const oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize); @@ -510,6 +523,7 @@ export class AutoIndentOnPaste implements IEditorContribution { } } + console.log('textEdits : ', textEdits); if (textEdits.length > 0) { this.editor.pushUndoStop(); const cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); @@ -519,19 +533,25 @@ export class AutoIndentOnPaste implements IEditorContribution { } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { + console.log('shouldIgnoreLine'); model.tokenization.forceTokenization(lineNumber); const nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); + console.log('nonWhitespaceColumn : ', nonWhitespaceColumn); if (nonWhitespaceColumn === 0) { return true; } const tokens = model.tokenization.getLineTokens(lineNumber); + console.log('tokens : ', JSON.stringify(tokens)); + if (tokens.getCount() > 0) { const firstNonWhitespaceTokenIndex = tokens.findTokenIndexAtOffset(nonWhitespaceColumn); + console.log('firstNonWhitespaceTokenIndex : ', firstNonWhitespaceTokenIndex); if (firstNonWhitespaceTokenIndex >= 0 && tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) === StandardTokenType.Comment) { return true; } } + console.log('return false'); return false; } diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 731f1edbf69..2bd3cef54b7 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -19,6 +19,7 @@ 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 { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; 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); @@ -30,49 +31,12 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab suite('Indentation - TypeScript/Javascript', () => { - const languageId = 'ts-test'; + let languageId: string; let disposables: DisposableStore; setup(() => { disposables = new DisposableStore(); - const instantiationService = createModelServices(disposables); - 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 - })); + languageId = 'ts-test'; }); teardown(() => { @@ -83,6 +47,11 @@ suite('Indentation - TypeScript/Javascript', () => { suite('Change Indentation to Spaces', () => { + setup(() => { + const instantiationService = createModelServices(disposables); + instantiateContext(instantiationService, true); + }); + test('single tabs only at start of line', function () { testIndentationToSpacesCommand( [ @@ -254,11 +223,11 @@ suite('Indentation - TypeScript/Javascript', () => { suite('Auto Indent On Paste', () => { 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) => { + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + instantiateContext(instantiationService); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); const pasteText = [ '/**', @@ -277,11 +246,11 @@ suite('Indentation - TypeScript/Javascript', () => { suite('Keep Indent On Paste', () => { test('issue #167299: Blank line removes indent', () => { - const languageId = 'blankLineRemovesIndent'; const model = createTextModel("", languageId, {}); disposables.add(model); withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + instantiateContext(instantiationService); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); const pasteText = [ '', @@ -302,13 +271,112 @@ suite('Indentation - TypeScript/Javascript', () => { assert.strictEqual(model.getValue(), pasteText); }); }); + + // TODO: tokenization is not corerct, hence why the test does not reflect the real behavior + test('issue #181065: Incorrect paste', () => { + const model = createTextModel("", languageId, {}); + disposables.add(model); + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + + instantiateContext(instantiationService); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + const pasteText = [ + '/**', + ' * @typedef {', + ' * }', + ' */', + ].join('\n'); + + viewModel.paste(pasteText, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4)); + assert.strictEqual(model.getValue(), pasteText); + }); + }); }); + + // suite('Auto Indent On Type', () => { + // test('issue #193875: incorrect indentation', () => { + // const model = createTextModel([ + // '{', + // ' for(;;)', + // ' for(;;) {}', + // '}' + // ].join('\n'), languageId, {}); + // disposables.add(model); + // withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + // instantiateContext(instantiationService); + // // viewModel.type([ + // // '{', + // // ' for(;;)', + // // ' for(;;) {}', + // // '}' + // // ].join('\n')); + // viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); + // viewModel.type("\n", 'keyboard'); + // assert.strictEqual(model.getValue(), [ + // '{', + // ' for(;;)', + // ' for(;;) {', + // '}', + // '}' + // ].join('\n')); + // }); + // }); + // }); + + function instantiateContext(instantiationService: TestInstantiationService, includeTokenization: boolean = false) { + const languageService = instantiationService.get(ILanguageService); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + indentationRules: javascriptIndentationRules, + onEnterRules: javascriptOnEnterRules + })); + if (includeTokenization) { + disposables.add(TokenizationRegistry.register(languageId, { + getInitialState: (): IState => NullState, + tokenize: () => { + throw new Error('not implemented'); + }, + tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { + console.log('tokenizeEncoded', line, hasEOL, state); + const tokensArr: number[] = []; + if (line.indexOf('*') !== -1) { + console.log('first'); + tokensArr.push(0); + tokensArr.push(StandardTokenType.Comment << MetadataConsts.TOKEN_TYPE_OFFSET); + } else { + console.log('second'); + 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); + } + })); + } + } }); suite('Indentation - Ruby', () => { + + let languageId: string; let disposables: DisposableStore; setup(() => { + languageId = 'ruby-test'; disposables = new DisposableStore(); }); @@ -318,48 +386,16 @@ suite('Indentation - Ruby', () => { ensureNoDisposablesAreLeakedInTestSuite(); - suite('Indent On Type', () => { + suite('Auto Indent On Type', () => { 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(); + instantiateContext(instantiationService); - 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"); @@ -371,8 +407,24 @@ suite('Indentation - Ruby', () => { assert.strictEqual(model.getValue(), " # in"); viewModel.type(" ", 'keyboard'); assert.strictEqual(model.getValue(), " # in "); - improvedLanguageModel.dispose(); }); }); }); + + function instantiateContext(instantiationService: TestInstantiationService) { + const languageService = instantiationService.get(ILanguageService); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + disposables.add(languageService.registerLanguage({ id: languageId })); + 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)*(#.*)?$/, + }, + })); + } }); From f71d90f052d01cff558aca8f1ebfbd6344f53251 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 18 Mar 2024 16:17:22 +0100 Subject: [PATCH 08/47] using again the initial split, because currently not able to combine the tests --- .../test/browser/indentation.test.ts | 816 ++++++++++-------- 1 file changed, 438 insertions(+), 378 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 2bd3cef54b7..154cd68a303 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -6,20 +6,31 @@ 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 { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + +// optional: could move the workbench code to editor folder +// pull the two test files together +// for the failing tests add .skip +// Then start changing the regex +// Then only after change the code + +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); @@ -29,354 +40,250 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -suite('Indentation - TypeScript/Javascript', () => { +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 })); +} - let languageId: string; - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - languageId = 'ts-test'; - }); - - teardown(() => { - disposables.dispose(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('Change Indentation to Spaces', () => { - - setup(() => { - const instantiationService = createModelServices(disposables); - instantiateContext(instantiationService, true); - }); - - test('single tabs only at start of line', function () { - testIndentationToSpacesCommand( - [ - 'first', - 'second line', - 'third line', - '\tfourth line', - '\tfifth' +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: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] ], - new Selection(2, 3, 2, 3), - 4, - [ - 'first', - 'second line', - 'third line', - ' fourth line', - ' fifth' - ], - new Selection(2, 3, 2, 3) - ); - }); - - test('multiple tabs at start of line', function () { - testIndentationToSpacesCommand( - [ - '\t\tfirst', - '\tsecond line', - '\t\t\t third line', - 'fourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5), - 3, - [ - ' first', - ' second line', - ' third line', - 'fourth line', - 'fifth' - ], - new Selection(1, 9, 1, 9) - ); - }); - - test('multiple tabs', function () { - testIndentationToSpacesCommand( - [ - '\t\tfirst\t', - '\tsecond \t line \t', - '\t\t\t third line', - ' \tfourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5), - 2, - [ - ' first\t', - ' second \t line \t', - ' third line', - ' fourth line', - 'fifth' - ], - new Selection(1, 7, 1, 7) - ); - }); - - test('empty lines', function () { - testIndentationToSpacesCommand( - [ - '\t\t\t', - '\t', - '\t\t' - ], - new Selection(1, 4, 1, 4), - 2, - [ - ' ', - ' ', - ' ' - ], - new Selection(1, 4, 1, 4) - ); - }); - }); - - suite('Change Indentation to Tabs', () => { - - test('spaces only at start of line', function () { - testIndentationToTabsCommand( - [ - ' first', - 'second line', - ' third line', - 'fourth line', - 'fifth' - ], - new Selection(2, 3, 2, 3), - 4, - [ - '\tfirst', - 'second line', - '\tthird line', - 'fourth line', - 'fifth' - ], - new Selection(2, 3, 2, 3) - ); - }); - - test('multiple spaces at start of line', function () { - testIndentationToTabsCommand( - [ - 'first', - ' second line', - ' third line', - 'fourth line', - ' fifth' - ], - new Selection(1, 5, 1, 5), - 3, - [ - 'first', - '\tsecond line', - '\t\t\t third line', - 'fourth line', - '\t fifth' - ], - new Selection(1, 5, 1, 5) - ); - }); - - test('multiple spaces', function () { - testIndentationToTabsCommand( - [ - ' first ', - ' second line \t', - ' third line', - ' fourth line', - 'fifth' - ], - new Selection(1, 8, 1, 8), - 2, - [ - '\t\t\tfirst ', - '\tsecond line \t', - '\t\t\t third line', - '\t fourth line', - 'fifth' - ], - new Selection(1, 5, 1, 5) - ); - }); - - test('issue #45996', function () { - testIndentationToSpacesCommand( - [ - '\tabc', - ], - new Selection(1, 3, 1, 3), - 4, - [ - ' abc', - ], - new Selection(1, 6, 1, 6) - ); - }); - }); - - suite('Auto Indent On Paste', () => { - - test('issue #119225: Do not add extra leading space when pasting JSDoc', () => { - const model = createTextModel("", languageId, {}); - disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - - instantiateContext(instantiationService); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); - const pasteText = [ - '/**', - ' * JSDoc', - ' */', - 'function a() {}' - ].join('\n'); - - viewModel.paste(pasteText, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); - assert.strictEqual(model.getValue(), pasteText); - }); - }); - }); - - suite('Keep Indent On Paste', () => { - - test('issue #167299: Blank line removes indent', () => { - const model = createTextModel("", languageId, {}); - disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - - instantiateContext(instantiationService); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); - const pasteText = [ - '', - 'export type IncludeReference =', - ' | BaseReference', - ' | SelfReference', - ' | RelativeReference;', - '', - 'export const enum IncludeReferenceKind {', - ' Base,', - ' Self,', - ' RelativeReference,', - '}' - ].join('\n'); - - viewModel.paste(pasteText, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); - assert.strictEqual(model.getValue(), pasteText); - }); - }); - - // TODO: tokenization is not corerct, hence why the test does not reflect the real behavior - test('issue #181065: Incorrect paste', () => { - const model = createTextModel("", languageId, {}); - disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - - instantiateContext(instantiationService); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); - const pasteText = [ - '/**', - ' * @typedef {', - ' * }', - ' */', - ].join('\n'); - - viewModel.paste(pasteText, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4)); - assert.strictEqual(model.getValue(), pasteText); - }); - }); - }); - - // suite('Auto Indent On Type', () => { - // test('issue #193875: incorrect indentation', () => { - // const model = createTextModel([ - // '{', - // ' for(;;)', - // ' for(;;) {}', - // '}' - // ].join('\n'), languageId, {}); - // disposables.add(model); - // withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - // instantiateContext(instantiationService); - // // viewModel.type([ - // // '{', - // // ' for(;;)', - // // ' for(;;) {}', - // // '}' - // // ].join('\n')); - // viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); - // viewModel.type("\n", 'keyboard'); - // assert.strictEqual(model.getValue(), [ - // '{', - // ' for(;;)', - // ' for(;;) {', - // '}', - // '}' - // ].join('\n')); - // }); - // }); - // }); - - function instantiateContext(instantiationService: TestInstantiationService, includeTokenization: boolean = false) { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - comments: { - lineComment: '//', - blockComment: ['/*', '*/'] - }, - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); - if (includeTokenization) { - disposables.add(TokenizationRegistry.register(languageId, { - getInitialState: (): IState => NullState, - tokenize: () => { - throw new Error('not implemented'); + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] }, - tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { - console.log('tokenizeEncoded', line, hasEOL, state); - const tokensArr: number[] = []; - if (line.indexOf('*') !== -1) { - console.log('first'); - tokensArr.push(0); - tokensArr.push(StandardTokenType.Comment << MetadataConsts.TOKEN_TYPE_OFFSET); - } else { - console.log('second'); - 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); - } + 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(); + + test('single tabs only at start of line', function () { + testIndentationToSpacesCommand( + [ + 'first', + 'second line', + 'third line', + '\tfourth line', + '\tfifth' + ], + new Selection(2, 3, 2, 3), + 4, + [ + 'first', + 'second line', + 'third line', + ' fourth line', + ' fifth' + ], + new Selection(2, 3, 2, 3) + ); + }); + + test('multiple tabs at start of line', function () { + testIndentationToSpacesCommand( + [ + '\t\tfirst', + '\tsecond line', + '\t\t\t third line', + 'fourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5), + 3, + [ + ' first', + ' second line', + ' third line', + 'fourth line', + 'fifth' + ], + new Selection(1, 9, 1, 9) + ); + }); + + test('multiple tabs', function () { + testIndentationToSpacesCommand( + [ + '\t\tfirst\t', + '\tsecond \t line \t', + '\t\t\t third line', + ' \tfourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5), + 2, + [ + ' first\t', + ' second \t line \t', + ' third line', + ' fourth line', + 'fifth' + ], + new Selection(1, 7, 1, 7) + ); + }); + + test('empty lines', function () { + testIndentationToSpacesCommand( + [ + '\t\t\t', + '\t', + '\t\t' + ], + new Selection(1, 4, 1, 4), + 2, + [ + ' ', + ' ', + ' ' + ], + new Selection(1, 4, 1, 4) + ); + }); }); -suite('Indentation - Ruby', () => { +suite('Change Indentation to Tabs - TypeScript/Javascript', () => { - let languageId: string; + ensureNoDisposablesAreLeakedInTestSuite(); + + test('spaces only at start of line', function () { + testIndentationToTabsCommand( + [ + ' first', + 'second line', + ' third line', + 'fourth line', + 'fifth' + ], + new Selection(2, 3, 2, 3), + 4, + [ + '\tfirst', + 'second line', + '\tthird line', + 'fourth line', + 'fifth' + ], + new Selection(2, 3, 2, 3) + ); + }); + + test('multiple spaces at start of line', function () { + testIndentationToTabsCommand( + [ + 'first', + ' second line', + ' third line', + 'fourth line', + ' fifth' + ], + new Selection(1, 5, 1, 5), + 3, + [ + 'first', + '\tsecond line', + '\t\t\t third line', + 'fourth line', + '\t fifth' + ], + new Selection(1, 5, 1, 5) + ); + }); + + test('multiple spaces', function () { + testIndentationToTabsCommand( + [ + ' first ', + ' second line \t', + ' third line', + ' fourth line', + 'fifth' + ], + new Selection(1, 8, 1, 8), + 2, + [ + '\t\t\tfirst ', + '\tsecond line \t', + '\t\t\t third line', + '\t fourth line', + 'fifth' + ], + new Selection(1, 5, 1, 5) + ); + }); + + test('issue #45996', function () { + testIndentationToSpacesCommand( + [ + '\tabc', + ], + new Selection(1, 3, 1, 3), + 4, + [ + ' abc', + ], + new Selection(1, 6, 1, 6) + ); + }); +}); + +suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { + + const languageId = 'ts-test'; let disposables: DisposableStore; setup(() => { - languageId = 'ruby-test'; disposables = new DisposableStore(); }); @@ -386,45 +293,198 @@ suite('Indentation - Ruby', () => { ensureNoDisposablesAreLeakedInTestSuite(); - suite('Auto Indent On Type', () => { + test('issue #119225: Do not add extra leading space when pasting JSDoc', () => { - test('issue #198350: in or when incorrectly match non keywords for Ruby', () => { - const model = createTextModel("", languageId, {}); - disposables.add(model); + const model = createTextModel("", languageId, {}); + disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - instantiateContext(instantiationService); - - viewModel.type("def foo\n i"); - viewModel.type("n", 'keyboard'); - assert.strictEqual(model.getValue(), "def foo\n in"); - viewModel.type(" ", 'keyboard'); - assert.strictEqual(model.getValue(), "def foo\nin "); - - viewModel.model.setValue(""); - viewModel.type(" # in"); - assert.strictEqual(model.getValue(), " # in"); - viewModel.type(" ", 'keyboard'); - assert.strictEqual(model.getValue(), " # in "); - }); + 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); }); }); - function instantiateContext(instantiationService: TestInstantiationService) { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - 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)*(#.*)?$/, - }, - })); - } + test('issue #167299: Blank line removes indent', () => { + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + const pasteText = [ + '', + 'export type IncludeReference =', + ' | BaseReference', + ' | SelfReference', + ' | RelativeReference;', + '', + 'export const enum IncludeReferenceKind {', + ' Base,', + ' Self,', + ' RelativeReference,', + '}' + ].join('\n'); + // no need for tokenization because there are no comments + + 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); + }); + }); + + test('issue #181065: Incorrect paste of object within comment', () => { + + 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); + }); + }); +}); + +// suite('Auto Indent On Type - TypeScript/JavaScript', () => { +// test('issue #193875: incorrect indentation', () => { +// const model = createTextModel([ +// '{', +// ' for(;;)', +// ' for(;;) {}', +// '}' +// ].join('\n'), languageId, {}); +// disposables.add(model); +// withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { +// instantiateContext(instantiationService); +// // viewModel.type([ +// // '{', +// // ' for(;;)', +// // ' for(;;) {}', +// // '}' +// // ].join('\n')); +// viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); +// viewModel.type("\n", 'keyboard'); +// assert.strictEqual(model.getValue(), [ +// '{', +// ' for(;;)', +// ' for(;;) {', +// '}', +// '}' +// ].join('\n')); +// }); +// }); +// }); + +suite('Auto Dedent On Type - Ruby', () => { + + const languageId = "ruby-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('issue #198350: in or when incorrectly match non keywords for Ruby', () => { + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.Ruby, disposables); + + // TODO@aiday-mar: requires registering the tokens ? + + viewModel.model.setValue(""); + viewModel.type("def foo\n i"); + viewModel.type("n", 'keyboard'); + assert.strictEqual(model.getValue(), "def foo\n in"); + viewModel.type(" ", 'keyboard'); + assert.strictEqual(model.getValue(), "def foo\nin "); + + viewModel.model.setValue(""); + viewModel.type(" # in"); + assert.strictEqual(model.getValue(), " # in"); + viewModel.type(" ", 'keyboard'); + assert.strictEqual(model.getValue(), " # in "); + }); + }); }); From d5f5d2212aac93ac25ee3f4fe04253bdf268119f Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 18 Mar 2024 18:10:22 +0100 Subject: [PATCH 09/47] adding more tests --- .../test/browser/indentation.test.ts | 204 +++++++++++++++--- .../codeEditor/test/node/autoindent.test.ts | 28 +++ 2 files changed, 200 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 154cd68a303..03e27c967c6 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -26,6 +26,7 @@ import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/jav // for the failing tests add .skip // Then start changing the regex // Then only after change the code +// Find how to combine the tests later enum Language { TypeScript, @@ -46,6 +47,7 @@ function registerLanguage(instantiationService: TestInstantiationService, langua 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) { @@ -370,7 +372,9 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); - test('issue #181065: Incorrect paste of object within comment', () => { + // TODO@aiday-mar: failing test + + test.skip('issue #181065: Incorrect paste of object within comment', () => { const model = createTextModel("", languageId, {}); disposables.add(model); @@ -415,39 +419,176 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { assert.strictEqual(model.getValue(), text); }); }); + + test('issue #86301: indent trailing new line', () => { + + 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(1, 1, 6, 2)); + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' ', + ' }', + ' ', + '}', + ].join('\n')); + }); + }); + + test('issue #85781: indent correctly new line on which pasted', () => { + + 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(1, 1, 6, 2)); + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' console.log("b")', + ' }', + ' console.log("a");', + '}', + ].join('\n')); + }); + }); }); -// suite('Auto Indent On Type - TypeScript/JavaScript', () => { -// test('issue #193875: incorrect indentation', () => { -// const model = createTextModel([ -// '{', -// ' for(;;)', -// ' for(;;) {}', -// '}' -// ].join('\n'), languageId, {}); -// disposables.add(model); -// withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { -// instantiateContext(instantiationService); -// // viewModel.type([ -// // '{', -// // ' for(;;)', -// // ' for(;;) {}', -// // '}' -// // ].join('\n')); -// viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); -// viewModel.type("\n", 'keyboard'); -// assert.strictEqual(model.getValue(), [ -// '{', -// ' for(;;)', -// ' for(;;) {', -// '}', -// '}' -// ].join('\n')); -// }); -// }); -// }); +suite('Auto Indent On Type - TypeScript/JavaScript', () => { -suite('Auto Dedent On Type - Ruby', () => { + // test('issue #193875: incorrect indentation', () => { + // const model = createTextModel([ + // '{', + // ' for(;;)', + // ' for(;;) {}', + // '}' + // ].join('\n'), languageId, {}); + // disposables.add(model); + // withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + // instantiateContext(instantiationService); + // // viewModel.type([ + // // '{', + // // ' for(;;)', + // // ' for(;;) {}', + // // '}' + // // ].join('\n')); + // viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); + // viewModel.type("\n", 'keyboard'); + // assert.strictEqual(model.getValue(), [ + // '{', + // ' for(;;)', + // ' for(;;) {', + // '}', + // '}' + // ].join('\n')); + // }); + // }); + + const languageId = "ts-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // TODO@aiday-mar: failing test + + test.skip('issue #116843: dedent after arrow function', () => { + + 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 where created', () => { + + 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('issue #40115: keep indentation where created', () => { + // Add tests for: + // https://github.com/microsoft/vscode/issues/88638 + // https://github.com/microsoft/vscode/issues/63388 + // https://github.com/microsoft/vscode/issues/46401 + }); +}); + +suite('Auto Indent On Type - Ruby', () => { const languageId = "ruby-test"; let disposables: DisposableStore; @@ -473,7 +614,6 @@ suite('Auto Dedent On Type - Ruby', () => { // TODO@aiday-mar: requires registering the tokens ? - viewModel.model.setValue(""); viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 132e120c755..76f2e74a05e 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -271,6 +271,34 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { assert.deepEqual(editOperations.length, 0); }); + test('Issue #116843', () => { + + // issue: https://github.com/microsoft/vscode/issues/116843 + // explanation: When you have an arrow function, you don't have { or }, but you would expect indentation to still be done in that way + + 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.deepEqual(editOperations.length, 0); + }); + + test('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.deepEqual(editOperations.length, 0); + }); }); function getIRange(range: IRange): IRange { From 9a63e6829c4c520d3902791146cd2fdb177d8605 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 19 Mar 2024 12:21:09 +0100 Subject: [PATCH 10/47] adding more indentation tests --- .../test/browser/indentation.test.ts | 152 ++++++++++++++---- .../codeEditor/test/node/autoindent.test.ts | 1 + 2 files changed, 125 insertions(+), 28 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 03e27c967c6..c962f27a373 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -484,38 +484,72 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n')); }); }); + + test('issue #29589: incorrect preservation of indentation', () => { + + // 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')); + }); + }); + + test('issue #65614: incorrect indentation on paste', () => { + + // https://github.com/microsoft/vscode/issues/65614 + + const model = createTextModel([ + 'if(true) {', + '', + '}' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 1, 2, 1)); + const text = [ + 'if(false) {', + '', + '}' + ].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, 5, 2)); + assert.strictEqual(model.getValue(), [ + 'if(true) {', + ' if(false) {', + ' ', + ' }', + '}' + ].join('\n')); + }); + }); }); suite('Auto Indent On Type - TypeScript/JavaScript', () => { - // test('issue #193875: incorrect indentation', () => { - // const model = createTextModel([ - // '{', - // ' for(;;)', - // ' for(;;) {}', - // '}' - // ].join('\n'), languageId, {}); - // disposables.add(model); - // withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - // instantiateContext(instantiationService); - // // viewModel.type([ - // // '{', - // // ' for(;;)', - // // ' for(;;) {}', - // // '}' - // // ].join('\n')); - // viewModel.setSelections('test', [new Selection(3, 11, 3, 11)]); - // viewModel.type("\n", 'keyboard'); - // assert.strictEqual(model.getValue(), [ - // '{', - // ' for(;;)', - // ' for(;;) {', - // '}', - // '}' - // ].join('\n')); - // }); - // }); - const languageId = "ts-test"; let disposables: DisposableStore; @@ -580,11 +614,73 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + test('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'); + // is the indentation actually done here? + assert.strictEqual(model.getValue(), [ + '{', + ' for(;;)', + ' for(;;) {', + '', + ' }', + '}', + ].join('\n')); + }); + }); + + test('issue #43244: incorrect indentation', () => { + + // https://github.com/microsoft/vscode/issues/193875 + // https://github.com/microsoft/vscode/issues/43244 + + const model = createTextModel([ + '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(2, 12, 2, 12)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (condition)', + ' return;', + '', + ].join('\n')); + + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (condition)', + ' return;', + '', + '' + ].join('\n')); + }); + }); + test('issue #40115: keep indentation where created', () => { // 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 }); }); diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 76f2e74a05e..6c216541a61 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -274,6 +274,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { test('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 const fileContents = [ From 28069298757270e3810fd1983d7ce08f1395d8f2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 19 Mar 2024 14:53:29 +0100 Subject: [PATCH 11/47] adding changes, remove console logs later --- .../common/cursor/cursorTypeOperations.ts | 13 ++- .../editor/common/encodedTokenAttributes.ts | 4 + src/vs/editor/common/languages/autoIndent.ts | 46 +++++++- .../common/languages/supports/onEnter.ts | 2 + src/vs/editor/common/tokens/lineTokens.ts | 3 + .../indentation/browser/indentation.ts | 17 ++- .../test/browser/indentation.test.ts | 104 +++++++----------- .../codeEditor/test/node/autoindent.test.ts | 44 ++++---- 8 files changed, 140 insertions(+), 93 deletions(-) diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index e71f02a960e..cc54b2f9b0e 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -297,12 +297,16 @@ export class TypeOperations { } private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { + console.log('inside of _enter'); + if (config.autoIndent === EditorAutoIndentStrategy.None) { + console.log('return 1'); return TypeOperations._typeCommand(range, '\n', keepPosition); } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { const lineText = model.getLineContent(range.startLineNumber); const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + console.log('return 2'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); } @@ -310,10 +314,12 @@ export class TypeOperations { if (r) { if (r.indentAction === IndentAction.None) { // Nothing special + console.log('return 3'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); } else if (r.indentAction === IndentAction.Indent) { // Indent once + console.log('return 4'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); } else if (r.indentAction === IndentAction.IndentOutdent) { @@ -324,12 +330,15 @@ export class TypeOperations { const typeText = '\n' + increasedIndent + '\n' + normalIndent; if (keepPosition) { + console.log('return 5'); return new ReplaceCommandWithoutChangingPosition(range, typeText, true); } else { + console.log('return 6'); return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); } } else if (r.indentAction === IndentAction.Outdent) { const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation); + console.log('return 7'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); } } @@ -362,6 +371,7 @@ export class TypeOperations { } if (keepPosition) { + console.log('return 8'); return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; @@ -371,11 +381,12 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } + console.log('return 9'); return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } - + console.log('return 10'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); } diff --git a/src/vs/editor/common/encodedTokenAttributes.ts b/src/vs/editor/common/encodedTokenAttributes.ts index c0f2fad70f1..7d9ac2be41b 100644 --- a/src/vs/editor/common/encodedTokenAttributes.ts +++ b/src/vs/editor/common/encodedTokenAttributes.ts @@ -103,6 +103,10 @@ export class TokenMetadata { } public static getTokenType(metadata: number): StandardTokenType { + console.log('metadata : ', metadata); + console.log('MetadataConsts.TOKEN_TYPE_MASK : ', MetadataConsts.TOKEN_TYPE_MASK); + console.log('MetadataConsts.TOKEN_TYPE_OFFSET : ', MetadataConsts.TOKEN_TYPE_OFFSET); + console.log('(metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET : ', (metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET); return (metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET; } diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index 9ae3df974aa..b3619ef1f3f 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -77,16 +77,20 @@ export function getInheritIndentForLine( honorIntentialIndent: boolean = true, languageConfigurationService: ILanguageConfigurationService ): { indentation: string; action: IndentAction | null; line?: number } | null { + console.log('getInheritIndentForLine'); if (autoIndent < EditorAutoIndentStrategy.Full) { + console.log('return 1'); return null; } const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { + console.log('return 2'); return null; } if (lineNumber <= 1) { + console.log('return 3'); return { indentation: '', action: null @@ -99,6 +103,7 @@ export function getInheritIndentForLine( break; } if (priorLineNumber === 1) { + console.log('return 4'); return { indentation: '', action: null @@ -107,9 +112,13 @@ export function getInheritIndentForLine( } const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport); + console.log('precedingUnIgnoredLine : ', precedingUnIgnoredLine); + if (precedingUnIgnoredLine < 0) { + console.log('return 5'); return null; } else if (precedingUnIgnoredLine < 1) { + console.log('return 6'); return { indentation: '', action: null @@ -118,12 +127,14 @@ export function getInheritIndentForLine( const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { + console.log('return 7'); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: IndentAction.Indent, line: precedingUnIgnoredLine }; } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { + console.log('return 8'); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: null, @@ -136,6 +147,7 @@ export function getInheritIndentForLine( // so current line is not affect by precedingUnIgnoredLine // and then we should get a correct inheritted indentation from above lines if (precedingUnIgnoredLine === 1) { + console.log('return 9'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), action: null, @@ -156,7 +168,7 @@ export function getInheritIndentForLine( stopLine = i; break; } - + console.log('return 10'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), action: null, @@ -165,6 +177,7 @@ export function getInheritIndentForLine( } if (honorIntentialIndent) { + console.log('return 11'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), action: null, @@ -175,6 +188,7 @@ export function getInheritIndentForLine( for (let i = precedingUnIgnoredLine; i > 0; i--) { const lineContent = model.getLineContent(i); if (indentRulesSupport.shouldIncrease(lineContent)) { + console.log('return 12'); return { indentation: strings.getLeadingWhitespace(lineContent), action: IndentAction.Indent, @@ -189,13 +203,14 @@ export function getInheritIndentForLine( stopLine = j; break; } - + console.log('return 13'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), action: null, line: stopLine + 1 }; } else if (indentRulesSupport.shouldDecrease(lineContent)) { + console.log('return 14'); return { indentation: strings.getLeadingWhitespace(lineContent), action: null, @@ -203,7 +218,7 @@ export function getInheritIndentForLine( }; } } - + console.log('return 15'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(1)), action: null, @@ -221,23 +236,30 @@ export function getGoodIndentForLine( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { + console.log('getGoodIndentForLine'); + if (autoIndent < EditorAutoIndentStrategy.Full) { + console.log('return 1'); return null; } const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId); if (!richEditSupport) { + console.log('return 2'); return null; } const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { + console.log('return 3'); return null; } const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService); const lineContent = virtualModel.getLineContent(lineNumber); + console.log('indent : ', indent); + if (indent) { const inheritLine = indent.line; if (inheritLine !== undefined) { @@ -276,6 +298,7 @@ export function getGoodIndentForLine( indentation += enterResult.appendText; } + console.log('return 4'); return strings.getLeadingWhitespace(indentation); } } @@ -283,18 +306,23 @@ export function getGoodIndentForLine( if (indentRulesSupport.shouldDecrease(lineContent)) { if (indent.action === IndentAction.Indent) { + console.log('return 5'); return indent.indentation; } else { + console.log('return 6'); return indentConverter.unshiftIndent(indent.indentation); } } else { if (indent.action === IndentAction.Indent) { + console.log('return 7'); return indentConverter.shiftIndent(indent.indentation); } else { + console.log('return 8'); return indent.indentation; } } } + console.log('return 9'); return null; } @@ -305,7 +333,9 @@ export function getIndentForEnter( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): { beforeEnter: string; afterEnter: string } | null { + console.log('getIndentForEnter'); if (autoIndent < EditorAutoIndentStrategy.Full) { + console.log('return 1'); return null; } model.tokenization.forceTokenization(range.startLineNumber); @@ -333,6 +363,7 @@ export function getIndentForEnter( const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; if (!indentRulesSupport) { + console.log('return 2'); return null; } @@ -364,6 +395,8 @@ export function getIndentForEnter( const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService); if (!afterEnterAction) { const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; + console.log('return 3'); + console.log('beforeEnter : ', beforeEnter); return { beforeEnter: beforeEnter, afterEnter: beforeEnter @@ -380,6 +413,9 @@ export function getIndentForEnter( afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); } + console.log('return 4'); + console.log('before enter : ', embeddedLanguage ? currentLineIndent : beforeEnterIndent); + console.log('after enter : ', afterEnterIndent); return { beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, afterEnter: afterEnterIndent @@ -398,6 +434,9 @@ export function getIndentActionForType( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { + + console.log('getIndentActionForType'); + if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } @@ -451,6 +490,7 @@ export function getIndentMetadata( lineNumber: number, languageConfigurationService: ILanguageConfigurationService ): number | null { + console.log('getIndentMetadata '); const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { return null; diff --git a/src/vs/editor/common/languages/supports/onEnter.ts b/src/vs/editor/common/languages/supports/onEnter.ts index 5952586199b..c1a8f6fa944 100644 --- a/src/vs/editor/common/languages/supports/onEnter.ts +++ b/src/vs/editor/common/languages/supports/onEnter.ts @@ -84,6 +84,7 @@ export class OnEnterSupport { for (let i = 0, len = this._brackets.length; i < len; i++) { const bracket = this._brackets[i]; if (bracket.openRegExp.test(beforeEnterText) && bracket.closeRegExp.test(afterEnterText)) { + console.log('return IndentAction.IndentOutdent'); return { indentAction: IndentAction.IndentOutdent }; } } @@ -97,6 +98,7 @@ export class OnEnterSupport { for (let i = 0, len = this._brackets.length; i < len; i++) { const bracket = this._brackets[i]; if (bracket.openRegExp.test(beforeEnterText)) { + console.log('return : ', IndentAction.Indent); return { indentAction: IndentAction.Indent }; } } diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index bdcda7cb180..456af51f887 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -102,7 +102,10 @@ export class LineTokens implements IViewLineTokens { } public getStandardTokenType(tokenIndex: number): StandardTokenType { + console.log('tokenIndex : ', tokenIndex); + console.log('this._tokens : ', JSON.stringify(this._tokens)); const metadata = this._tokens[(tokenIndex << 1) + 1]; + console.log('metadata : ', metadata); return TokenMetadata.getTokenType(metadata); } diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 00818bd978e..3a21768a5b5 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -378,8 +378,13 @@ export class AutoIndentOnPaste implements IEditorContribution { } this.callOnModel.add(this.editor.onDidPaste(({ range }) => { + console.log('range : ', range); this.trigger(range); + const model = this.editor.getModel(); console.log('this.editor.getModel()?.getValue(); : ', this.editor.getModel()?.getValue()); + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + console.log('line length for ', i, ' ', model?.getLineContent(i).length); + } })); } @@ -425,6 +430,7 @@ export class AutoIndentOnPaste implements IEditorContribution { break; } + console.log('startLineNumber : ', startLineNumber); if (startLineNumber > range.endLineNumber) { console.log('return 4'); return; @@ -476,6 +482,7 @@ export class AutoIndentOnPaste implements IEditorContribution { break; } + console.log('startLineNumber !== range.endLineNumber : ', startLineNumber !== range.endLineNumber); if (startLineNumber !== range.endLineNumber) { const virtualModel = { tokenization: { @@ -498,20 +505,24 @@ export class AutoIndentOnPaste implements IEditorContribution { } }; const indentOfSecondLine = getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter, this._languageConfigurationService); - + console.log('indentOfSecondLine : ', indentOfSecondLine?.length); if (indentOfSecondLine !== null) { const newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize); const oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize); + console.log('newSpaceCntOfSecondLine : ', newSpaceCntOfSecondLine); + console.log('oldSpaceCntOfSecondLine : ', oldSpaceCntOfSecondLine); if (newSpaceCntOfSecondLine !== oldSpaceCntOfSecondLine) { const spaceCntOffset = newSpaceCntOfSecondLine - oldSpaceCntOfSecondLine; for (let i = startLineNumber + 1; i <= range.endLineNumber; i++) { + console.log('i : ', i); const lineContent = model.getLineContent(i); const originalIndent = strings.getLeadingWhitespace(lineContent); const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize); const newSpacesCnt = originalSpacesCnt + spaceCntOffset; const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces); - + console.log('newIndent : ', newIndent.length); + console.log('originalIndent : ', originalIndent.length); if (newIndent !== originalIndent) { textEdits.push({ range: new Range(i, 1, i, originalIndent.length + 1), @@ -540,12 +551,14 @@ export class AutoIndentOnPaste implements IEditorContribution { if (nonWhitespaceColumn === 0) { return true; } + console.log('lineNumber : ', lineNumber); const tokens = model.tokenization.getLineTokens(lineNumber); console.log('tokens : ', JSON.stringify(tokens)); if (tokens.getCount() > 0) { const firstNonWhitespaceTokenIndex = tokens.findTokenIndexAtOffset(nonWhitespaceColumn); console.log('firstNonWhitespaceTokenIndex : ', firstNonWhitespaceTokenIndex); + console.log('tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) : ', tokens.getStandardTokenType(firstNonWhitespaceTokenIndex)); if (firstNonWhitespaceTokenIndex >= 0 && tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) === StandardTokenType.Comment) { return true; } diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index c962f27a373..dd810aedefb 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -21,13 +21,6 @@ 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'; -// optional: could move the workbench code to editor folder -// pull the two test files together -// for the failing tests add .skip -// Then start changing the regex -// Then only after change the code -// Find how to combine the tests later - enum Language { TypeScript, Ruby @@ -280,7 +273,7 @@ suite('Change Indentation to Tabs - TypeScript/Javascript', () => { }); }); -suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { +suite('`Full` Auto Indent On Paste - TypeScript/JavaScript', () => { const languageId = 'ts-test'; let disposables: DisposableStore; @@ -349,6 +342,8 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { disposables.add(model); withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + + // no need for tokenization because there are no comments const pasteText = [ '', 'export type IncludeReference =', @@ -362,7 +357,6 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { ' RelativeReference,', '}' ].join('\n'); - // no need for tokenization because there are no comments registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); @@ -372,10 +366,12 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); - // TODO@aiday-mar: failing test + // 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); @@ -420,7 +416,9 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); - test('issue #86301: indent trailing new line', () => { + test.skip('issue #86301: preserve cursor at inserted indentation level', () => { + + // https://github.com/microsoft/vscode/issues/86301 const model = createTextModel([ '() => {', @@ -440,19 +438,29 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); - autoIndentOnPasteController.trigger(new Range(1, 1, 6, 2)); + 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('issue #85781: indent correctly new line on which pasted', () => { + 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([ '() => {', @@ -473,7 +481,7 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { 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, 6, 2)); + autoIndentOnPasteController.trigger(new Range(2, 5, 5, 6)); assert.strictEqual(model.getValue(), [ '() => {', ' () => {', @@ -485,7 +493,7 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); - test('issue #29589: incorrect preservation of indentation', () => { + test.skip('issue #29589: incorrect indentation of closing brace on paste', () => { // https://github.com/microsoft/vscode/issues/29589 @@ -513,42 +521,9 @@ suite('Full Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n')); }); }); - - test('issue #65614: incorrect indentation on paste', () => { - - // https://github.com/microsoft/vscode/issues/65614 - - const model = createTextModel([ - 'if(true) {', - '', - '}' - ].join('\n'), languageId, {}); - disposables.add(model); - - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - editor.setSelection(new Selection(2, 1, 2, 1)); - const text = [ - 'if(false) {', - '', - '}' - ].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, 5, 2)); - assert.strictEqual(model.getValue(), [ - 'if(true) {', - ' if(false) {', - ' ', - ' }', - '}' - ].join('\n')); - }); - }); }); -suite('Auto Indent On Type - TypeScript/JavaScript', () => { +suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { const languageId = "ts-test"; let disposables: DisposableStore; @@ -563,9 +538,11 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ensureNoDisposablesAreLeakedInTestSuite(); - // TODO@aiday-mar: failing test + // Failing tests from issues... - test.skip('issue #116843: dedent after arrow function', () => { + test.skip('issue #116843: decrease indent after arrow function', () => { + + // https://github.com/microsoft/vscode/issues/116843 const model = createTextModel("", languageId, {}); disposables.add(model); @@ -587,7 +564,9 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - test.skip('issue #40115: keep indentation where created', () => { + test.skip('issue #40115: keep indentation when added', () => { + + // https://github.com/microsoft/vscode/issues/40115 const model = createTextModel('function foo() {}', languageId, {}); disposables.add(model); @@ -614,6 +593,8 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + // CURRENT ISSUE - continue from here + test('issue #193875: incorrect indentation on enter', () => { // https://github.com/microsoft/vscode/issues/193875 @@ -631,12 +612,11 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); editor.setSelection(new Selection(3, 14, 3, 14)); viewModel.type("\n", 'keyboard'); - // is the indentation actually done here? assert.strictEqual(model.getValue(), [ '{', ' for(;;)', ' for(;;) {', - '', + ' ', ' }', '}', ].join('\n')); @@ -675,13 +655,11 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - test('issue #40115: keep indentation where created', () => { - // 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 - }); + // 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', () => { diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 6c216541a61..a5ad3e106aa 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -16,6 +16,15 @@ import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/wor 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'; @@ -218,19 +227,16 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ].join('\n'); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); }); // Failing tests inferred from the current regexes... - test('Incorrect deindentation after `*/}` string', () => { + 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 - // todo@aiday-mar: would it make more sense to analyze the document from top to bottom to find matching [, {, (, in order to decide when to indent - // based on the work done by Henning - const fileContents = [ `const obj = {`, ` obj1: {`, @@ -240,13 +246,12 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ].join('\n'); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - console.log('editOperations : ', JSON.stringify(editOperations)); - assert.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); }); // Failing tests from issues... - test('Issue #56275', () => { + 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 @@ -259,7 +264,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ].join('\n'); let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); fileContents = [ 'function foo() {', @@ -268,10 +273,10 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ].join('\n'); model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); }); - test('Issue #116843', () => { + test.skip('Issue #116843', () => { // issue: https://github.com/microsoft/vscode/issues/116843 // related: https://github.com/microsoft/vscode/issues/43244 @@ -283,10 +288,10 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ].join('\n'); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - assert.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); }); - test('Issue #185252', () => { + test.skip('Issue #185252', () => { // issue: https://github.com/microsoft/vscode/issues/185252 // explanation: Reindenting the comment correctly @@ -294,19 +299,10 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 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.deepEqual(editOperations.length, 0); + assert.deepStrictEqual(editOperations.length, 0); }); }); - -function getIRange(range: IRange): IRange { - return { - startLineNumber: range.startLineNumber, - startColumn: range.startColumn, - endLineNumber: range.endLineNumber, - endColumn: range.endColumn - }; -} From d0ee720501de7d696a4bbf7fed3b59d21ac399b9 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 12:27:24 +0100 Subject: [PATCH 12/47] removing console logs --- .../common/cursor/cursorTypeOperations.ts | 13 +----- .../editor/common/encodedTokenAttributes.ts | 4 -- src/vs/editor/common/languages/autoIndent.ts | 46 ++----------------- src/vs/editor/common/languages/enterAction.ts | 11 +++++ .../common/languages/supports/onEnter.ts | 2 - src/vs/editor/common/tokens/lineTokens.ts | 3 -- .../indentation/browser/indentation.ts | 35 +------------- .../test/browser/indentation.test.ts | 36 +++++++++------ .../modes/supports/javascriptOnEnterRules.ts | 12 ++++- 9 files changed, 48 insertions(+), 114 deletions(-) diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index cc54b2f9b0e..e71f02a960e 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -297,16 +297,12 @@ export class TypeOperations { } private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { - console.log('inside of _enter'); - if (config.autoIndent === EditorAutoIndentStrategy.None) { - console.log('return 1'); return TypeOperations._typeCommand(range, '\n', keepPosition); } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { const lineText = model.getLineContent(range.startLineNumber); const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - console.log('return 2'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); } @@ -314,12 +310,10 @@ export class TypeOperations { if (r) { if (r.indentAction === IndentAction.None) { // Nothing special - console.log('return 3'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); } else if (r.indentAction === IndentAction.Indent) { // Indent once - console.log('return 4'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); } else if (r.indentAction === IndentAction.IndentOutdent) { @@ -330,15 +324,12 @@ export class TypeOperations { const typeText = '\n' + increasedIndent + '\n' + normalIndent; if (keepPosition) { - console.log('return 5'); return new ReplaceCommandWithoutChangingPosition(range, typeText, true); } else { - console.log('return 6'); return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); } } else if (r.indentAction === IndentAction.Outdent) { const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation); - console.log('return 7'); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); } } @@ -371,7 +362,6 @@ export class TypeOperations { } if (keepPosition) { - console.log('return 8'); return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; @@ -381,12 +371,11 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } - console.log('return 9'); return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } - console.log('return 10'); + return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); } diff --git a/src/vs/editor/common/encodedTokenAttributes.ts b/src/vs/editor/common/encodedTokenAttributes.ts index 7d9ac2be41b..c0f2fad70f1 100644 --- a/src/vs/editor/common/encodedTokenAttributes.ts +++ b/src/vs/editor/common/encodedTokenAttributes.ts @@ -103,10 +103,6 @@ export class TokenMetadata { } public static getTokenType(metadata: number): StandardTokenType { - console.log('metadata : ', metadata); - console.log('MetadataConsts.TOKEN_TYPE_MASK : ', MetadataConsts.TOKEN_TYPE_MASK); - console.log('MetadataConsts.TOKEN_TYPE_OFFSET : ', MetadataConsts.TOKEN_TYPE_OFFSET); - console.log('(metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET : ', (metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET); return (metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET; } diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index b3619ef1f3f..9ae3df974aa 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -77,20 +77,16 @@ export function getInheritIndentForLine( honorIntentialIndent: boolean = true, languageConfigurationService: ILanguageConfigurationService ): { indentation: string; action: IndentAction | null; line?: number } | null { - console.log('getInheritIndentForLine'); if (autoIndent < EditorAutoIndentStrategy.Full) { - console.log('return 1'); return null; } const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { - console.log('return 2'); return null; } if (lineNumber <= 1) { - console.log('return 3'); return { indentation: '', action: null @@ -103,7 +99,6 @@ export function getInheritIndentForLine( break; } if (priorLineNumber === 1) { - console.log('return 4'); return { indentation: '', action: null @@ -112,13 +107,9 @@ export function getInheritIndentForLine( } const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport); - console.log('precedingUnIgnoredLine : ', precedingUnIgnoredLine); - if (precedingUnIgnoredLine < 0) { - console.log('return 5'); return null; } else if (precedingUnIgnoredLine < 1) { - console.log('return 6'); return { indentation: '', action: null @@ -127,14 +118,12 @@ export function getInheritIndentForLine( const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { - console.log('return 7'); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: IndentAction.Indent, line: precedingUnIgnoredLine }; } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { - console.log('return 8'); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: null, @@ -147,7 +136,6 @@ export function getInheritIndentForLine( // so current line is not affect by precedingUnIgnoredLine // and then we should get a correct inheritted indentation from above lines if (precedingUnIgnoredLine === 1) { - console.log('return 9'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), action: null, @@ -168,7 +156,7 @@ export function getInheritIndentForLine( stopLine = i; break; } - console.log('return 10'); + return { indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), action: null, @@ -177,7 +165,6 @@ export function getInheritIndentForLine( } if (honorIntentialIndent) { - console.log('return 11'); return { indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), action: null, @@ -188,7 +175,6 @@ export function getInheritIndentForLine( for (let i = precedingUnIgnoredLine; i > 0; i--) { const lineContent = model.getLineContent(i); if (indentRulesSupport.shouldIncrease(lineContent)) { - console.log('return 12'); return { indentation: strings.getLeadingWhitespace(lineContent), action: IndentAction.Indent, @@ -203,14 +189,13 @@ export function getInheritIndentForLine( stopLine = j; break; } - console.log('return 13'); + return { indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), action: null, line: stopLine + 1 }; } else if (indentRulesSupport.shouldDecrease(lineContent)) { - console.log('return 14'); return { indentation: strings.getLeadingWhitespace(lineContent), action: null, @@ -218,7 +203,7 @@ export function getInheritIndentForLine( }; } } - console.log('return 15'); + return { indentation: strings.getLeadingWhitespace(model.getLineContent(1)), action: null, @@ -236,30 +221,23 @@ export function getGoodIndentForLine( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { - console.log('getGoodIndentForLine'); - if (autoIndent < EditorAutoIndentStrategy.Full) { - console.log('return 1'); return null; } const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId); if (!richEditSupport) { - console.log('return 2'); return null; } const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { - console.log('return 3'); return null; } const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService); const lineContent = virtualModel.getLineContent(lineNumber); - console.log('indent : ', indent); - if (indent) { const inheritLine = indent.line; if (inheritLine !== undefined) { @@ -298,7 +276,6 @@ export function getGoodIndentForLine( indentation += enterResult.appendText; } - console.log('return 4'); return strings.getLeadingWhitespace(indentation); } } @@ -306,23 +283,18 @@ export function getGoodIndentForLine( if (indentRulesSupport.shouldDecrease(lineContent)) { if (indent.action === IndentAction.Indent) { - console.log('return 5'); return indent.indentation; } else { - console.log('return 6'); return indentConverter.unshiftIndent(indent.indentation); } } else { if (indent.action === IndentAction.Indent) { - console.log('return 7'); return indentConverter.shiftIndent(indent.indentation); } else { - console.log('return 8'); return indent.indentation; } } } - console.log('return 9'); return null; } @@ -333,9 +305,7 @@ export function getIndentForEnter( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): { beforeEnter: string; afterEnter: string } | null { - console.log('getIndentForEnter'); if (autoIndent < EditorAutoIndentStrategy.Full) { - console.log('return 1'); return null; } model.tokenization.forceTokenization(range.startLineNumber); @@ -363,7 +333,6 @@ export function getIndentForEnter( const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; if (!indentRulesSupport) { - console.log('return 2'); return null; } @@ -395,8 +364,6 @@ export function getIndentForEnter( const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService); if (!afterEnterAction) { const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; - console.log('return 3'); - console.log('beforeEnter : ', beforeEnter); return { beforeEnter: beforeEnter, afterEnter: beforeEnter @@ -413,9 +380,6 @@ export function getIndentForEnter( afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); } - console.log('return 4'); - console.log('before enter : ', embeddedLanguage ? currentLineIndent : beforeEnterIndent); - console.log('after enter : ', afterEnterIndent); return { beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, afterEnter: afterEnterIndent @@ -434,9 +398,6 @@ export function getIndentActionForType( indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { - - console.log('getIndentActionForType'); - if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } @@ -490,7 +451,6 @@ export function getIndentMetadata( lineNumber: number, languageConfigurationService: ILanguageConfigurationService ): number | null { - console.log('getIndentMetadata '); const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { return null; diff --git a/src/vs/editor/common/languages/enterAction.ts b/src/vs/editor/common/languages/enterAction.ts index 447665fe816..6c70804e0a4 100644 --- a/src/vs/editor/common/languages/enterAction.ts +++ b/src/vs/editor/common/languages/enterAction.ts @@ -15,6 +15,8 @@ export function getEnterAction( range: Range, languageConfigurationService: ILanguageConfigurationService ): CompleteEnterAction | null { + console.log('getEnterAction'); + const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); const richEditSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId); if (!richEditSupport) { @@ -43,7 +45,16 @@ export function getEnterAction( } } + console.log('previousLineText : ', previousLineText); + console.log('previousLineText.length : ', previousLineText.length); + console.log('beforeEnterText : ', beforeEnterText); + console.log('beforeEnterText.length : ', beforeEnterText.length); + console.log('afterEnterText : ', afterEnterText); + console.log('afterEnterText.length : ', afterEnterText.length); + const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); + console.log('enterResult : ', JSON.stringify(enterResult)); + if (!enterResult) { return null; } diff --git a/src/vs/editor/common/languages/supports/onEnter.ts b/src/vs/editor/common/languages/supports/onEnter.ts index c1a8f6fa944..5952586199b 100644 --- a/src/vs/editor/common/languages/supports/onEnter.ts +++ b/src/vs/editor/common/languages/supports/onEnter.ts @@ -84,7 +84,6 @@ export class OnEnterSupport { for (let i = 0, len = this._brackets.length; i < len; i++) { const bracket = this._brackets[i]; if (bracket.openRegExp.test(beforeEnterText) && bracket.closeRegExp.test(afterEnterText)) { - console.log('return IndentAction.IndentOutdent'); return { indentAction: IndentAction.IndentOutdent }; } } @@ -98,7 +97,6 @@ export class OnEnterSupport { for (let i = 0, len = this._brackets.length; i < len; i++) { const bracket = this._brackets[i]; if (bracket.openRegExp.test(beforeEnterText)) { - console.log('return : ', IndentAction.Indent); return { indentAction: IndentAction.Indent }; } } diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index 456af51f887..bdcda7cb180 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -102,10 +102,7 @@ export class LineTokens implements IViewLineTokens { } public getStandardTokenType(tokenIndex: number): StandardTokenType { - console.log('tokenIndex : ', tokenIndex); - console.log('this._tokens : ', JSON.stringify(this._tokens)); const metadata = this._tokens[(tokenIndex << 1) + 1]; - console.log('metadata : ', metadata); return TokenMetadata.getTokenType(metadata); } diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 3a21768a5b5..1db0b5adb34 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -378,33 +378,22 @@ export class AutoIndentOnPaste implements IEditorContribution { } this.callOnModel.add(this.editor.onDidPaste(({ range }) => { - console.log('range : ', range); this.trigger(range); - const model = this.editor.getModel(); - console.log('this.editor.getModel()?.getValue(); : ', this.editor.getModel()?.getValue()); - for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { - console.log('line length for ', i, ' ', model?.getLineContent(i).length); - } })); } public trigger(range: Range): void { - console.log('trigger : ', JSON.stringify(range)); - const selections = this.editor.getSelections(); if (selections === null || selections.length > 1) { - console.log('return 1'); return; } const model = this.editor.getModel(); if (!model) { - console.log('return 2'); return; } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { - console.log('return 3'); return; } const autoIndent = this.editor.getOption(EditorOption.autoIndent); @@ -430,14 +419,11 @@ export class AutoIndentOnPaste implements IEditorContribution { break; } - console.log('startLineNumber : ', startLineNumber); if (startLineNumber > range.endLineNumber) { - console.log('return 4'); return; } let firstLineText = model.getLineContent(startLineNumber); - console.log('firstLineText : ', firstLineText); if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) { const indentOfFirstLine = getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter, this._languageConfigurationService); @@ -446,9 +432,6 @@ export class AutoIndentOnPaste implements IEditorContribution { const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize); const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize); - console.log('newSpaceCnt : ', newSpaceCnt); - console.log('oldSpaceCnt : ', oldSpaceCnt); - if (newSpaceCnt !== oldSpaceCnt) { const newIndent = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces); textEdits.push({ @@ -464,7 +447,6 @@ export class AutoIndentOnPaste implements IEditorContribution { // after pasting, the indentation of the first line is already correct // the first line doesn't match any indentation rule // then no-op. - console.log('return 5'); return; } } @@ -482,7 +464,6 @@ export class AutoIndentOnPaste implements IEditorContribution { break; } - console.log('startLineNumber !== range.endLineNumber : ', startLineNumber !== range.endLineNumber); if (startLineNumber !== range.endLineNumber) { const virtualModel = { tokenization: { @@ -505,24 +486,19 @@ export class AutoIndentOnPaste implements IEditorContribution { } }; const indentOfSecondLine = getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter, this._languageConfigurationService); - console.log('indentOfSecondLine : ', indentOfSecondLine?.length); if (indentOfSecondLine !== null) { const newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize); const oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize); - console.log('newSpaceCntOfSecondLine : ', newSpaceCntOfSecondLine); - console.log('oldSpaceCntOfSecondLine : ', oldSpaceCntOfSecondLine); if (newSpaceCntOfSecondLine !== oldSpaceCntOfSecondLine) { const spaceCntOffset = newSpaceCntOfSecondLine - oldSpaceCntOfSecondLine; for (let i = startLineNumber + 1; i <= range.endLineNumber; i++) { - console.log('i : ', i); const lineContent = model.getLineContent(i); const originalIndent = strings.getLeadingWhitespace(lineContent); const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize); const newSpacesCnt = originalSpacesCnt + spaceCntOffset; const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces); - console.log('newIndent : ', newIndent.length); - console.log('originalIndent : ', originalIndent.length); + if (newIndent !== originalIndent) { textEdits.push({ range: new Range(i, 1, i, originalIndent.length + 1), @@ -534,7 +510,6 @@ export class AutoIndentOnPaste implements IEditorContribution { } } - console.log('textEdits : ', textEdits); if (textEdits.length > 0) { this.editor.pushUndoStop(); const cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); @@ -544,27 +519,19 @@ export class AutoIndentOnPaste implements IEditorContribution { } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { - console.log('shouldIgnoreLine'); model.tokenization.forceTokenization(lineNumber); const nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); - console.log('nonWhitespaceColumn : ', nonWhitespaceColumn); if (nonWhitespaceColumn === 0) { return true; } - console.log('lineNumber : ', lineNumber); const tokens = model.tokenization.getLineTokens(lineNumber); - console.log('tokens : ', JSON.stringify(tokens)); - if (tokens.getCount() > 0) { const firstNonWhitespaceTokenIndex = tokens.findTokenIndexAtOffset(nonWhitespaceColumn); - console.log('firstNonWhitespaceTokenIndex : ', firstNonWhitespaceTokenIndex); - console.log('tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) : ', tokens.getStandardTokenType(firstNonWhitespaceTokenIndex)); if (firstNonWhitespaceTokenIndex >= 0 && tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) === StandardTokenType.Comment) { return true; } } - console.log('return false'); return false; } diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index dd810aedefb..21b41aa038b 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -538,6 +538,11 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { 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 #116843: decrease indent after arrow function', () => { @@ -593,9 +598,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - // CURRENT ISSUE - continue from here - - test('issue #193875: incorrect indentation on enter', () => { + test.skip('issue #193875: incorrect indentation on enter', () => { // https://github.com/microsoft/vscode/issues/193875 @@ -623,34 +626,39 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - test('issue #43244: incorrect indentation', () => { + test.skip('issue #43244: incorrect indentation', () => { - // https://github.com/microsoft/vscode/issues/193875 // https://github.com/microsoft/vscode/issues/43244 const model = createTextModel([ - 'if (condition)', - ' return;' + '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(2, 12, 2, 12)); + editor.setSelection(new Selection(3, 16, 3, 16)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ - 'if (condition)', - ' return;', + 'function f() {', + ' if (condition)', + ' return;', '', + '}', ].join('\n')); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ - 'if (condition)', - ' return;', + 'function f() {', + ' if (condition)', + ' return;', '', - '' + '', + '}', ].join('\n')); }); }); @@ -686,8 +694,6 @@ suite('Auto Indent On Type - Ruby', () => { registerLanguage(instantiationService, languageId, Language.Ruby, disposables); - // TODO@aiday-mar: requires registering the tokens ? - viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 5c8f580f228..12c05394dd9 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -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 } + }, ]; From c6b9216581d12904ba123225c12cd5af2a3ee4a2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 14:36:33 +0100 Subject: [PATCH 13/47] remove the comments --- src/vs/editor/common/languages/enterAction.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/vs/editor/common/languages/enterAction.ts b/src/vs/editor/common/languages/enterAction.ts index 6c70804e0a4..447665fe816 100644 --- a/src/vs/editor/common/languages/enterAction.ts +++ b/src/vs/editor/common/languages/enterAction.ts @@ -15,8 +15,6 @@ export function getEnterAction( range: Range, languageConfigurationService: ILanguageConfigurationService ): CompleteEnterAction | null { - console.log('getEnterAction'); - const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); const richEditSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId); if (!richEditSupport) { @@ -45,16 +43,7 @@ export function getEnterAction( } } - console.log('previousLineText : ', previousLineText); - console.log('previousLineText.length : ', previousLineText.length); - console.log('beforeEnterText : ', beforeEnterText); - console.log('beforeEnterText.length : ', beforeEnterText.length); - console.log('afterEnterText : ', afterEnterText); - console.log('afterEnterText.length : ', afterEnterText.length); - const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); - console.log('enterResult : ', JSON.stringify(enterResult)); - if (!enterResult) { return null; } From 8a22be5ddc3750155c3c756fcb784e649aa29fd3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 15:38:50 +0100 Subject: [PATCH 14/47] skiping the manual test --- .../workbench/contrib/codeEditor/test/node/autoindent.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index a5ad3e106aa..aab4ac02cdc 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -51,7 +51,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ensureNoDisposablesAreLeakedInTestSuite(); // Test which can be ran to find cases of incorrect indentation... - test('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(); From 30227752a7d62906923611a94dd11d68cb280cf5 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 16:20:26 +0100 Subject: [PATCH 15/47] adding changes to tests --- .../contrib/indentation/test/browser/indentation.test.ts | 9 ++++++--- .../contrib/codeEditor/test/node/autoindent.test.ts | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 21b41aa038b..73d33d9b97e 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -545,7 +545,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { // Failing tests from issues... - test.skip('issue #116843: decrease indent after arrow function', () => { + test('issue #116843: indent after arrow function', () => { // https://github.com/microsoft/vscode/issues/116843 @@ -556,10 +556,13 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - viewModel.type([ + viewModel.type('const add1 = (n) =>'); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ 'const add1 = (n) =>', - ' n + 1;', + ' ', ].join('\n')); + viewModel.type('n + 1;'); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ 'const add1 = (n) =>', diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index aab4ac02cdc..8f08b401e60 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -282,6 +282,10 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { // 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;', From 72d785f196711503f81beefff33b1d967f35be60 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 16:29:35 +0100 Subject: [PATCH 16/47] skipping the test --- .../editor/contrib/indentation/test/browser/indentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 73d33d9b97e..6b78736694f 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -545,7 +545,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { // Failing tests from issues... - test('issue #116843: indent after arrow function', () => { + test.skip('issue #116843: indent after arrow function', () => { // https://github.com/microsoft/vscode/issues/116843 From ab78ff55f4ee4d8334ece3e16e85c2e94b6e44da Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:00:23 +0100 Subject: [PATCH 17/47] Implement Display Protocol Detection (#208212) display protocol --- src/vs/base/node/osDisplayProtocolInfo.ts | 78 +++++++++++++++++++ .../node/sharedProcess/sharedProcessMain.ts | 15 +++- src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 1 + 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/vs/base/node/osDisplayProtocolInfo.ts diff --git a/src/vs/base/node/osDisplayProtocolInfo.ts b/src/vs/base/node/osDisplayProtocolInfo.ts new file mode 100644 index 00000000000..c028dc88536 --- /dev/null +++ b/src/vs/base/node/osDisplayProtocolInfo.ts @@ -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 { + 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; + } + } +} diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 81faf87e87b..9a59ccce5c3 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -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 { 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('clientPlatformInfo', { platformId: releaseInfo.id, platformVersionId: releaseInfo.version_id, platformIdLike: releaseInfo.id_like, - desktopEnvironment: desktopEnvironment + desktopEnvironment: desktopEnvironment, + displayProtocol: displayProtocol, + codeDisplayProtocol: codeSessionType }); } } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index cc157af7ab3..18d4fcd795a 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -140,4 +140,5 @@ export interface NativeParsedArgs { 'log-net-log'?: string; 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; + 'ozone-platform'?: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 0d3a9795ffb..327c18c18ad 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -204,6 +204,7 @@ export const OPTIONS: OptionDescriptions> = { '_urls': { type: 'string[]' }, 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, + 'ozone-platform': { type: 'string' }, _: { type: 'string[]' } // main arguments }; From 4f1d15d9daa6abd3d44a2043bf00262937f43a3c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 09:12:35 -0700 Subject: [PATCH 18/47] Use correct colors for highlights (#208214) I didn't realize there were 2 colors. This makes sure they're set correctly. Fixes https://github.com/microsoft/vscode/issues/208213 --- .../platform/quickinput/browser/media/quickInput.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index f8e7aba2b91..583f49ae26a 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -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 { From 342d7a0c0ceec77ace1a4b50c1da809c64d224a9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 09:25:02 -0700 Subject: [PATCH 19/47] Fixes #208207 (#208216) Fixes https://github.com/microsoft/vscode/issues/208207 --- src/vs/workbench/contrib/chat/browser/media/chat.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index eff556a5152..416b00d0484 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -501,6 +501,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 { From 5cc65d29ab3d77f6fedf322623ecdb9eb390bcad Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 17:41:07 +0100 Subject: [PATCH 20/47] arrow function --- .../language-configuration.json | 8 +++++- .../test/browser/indentation.test.ts | 25 ++++++++++++++++--- .../modes/supports/javascriptOnEnterRules.ts | 4 +++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index 2d14b1cab19..d2652244965 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -215,6 +215,12 @@ "action": { "indent": "outdent" } - } + }, + { + "beforeText": "^\\s*(var|const|let)\\s+\\w+\\s*=\\s*\\(.*\\)\\s*=>\\s*$", + "action": { + "indent": "indent" + } + }, ] } diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 6b78736694f..83ea2a23d0f 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -545,9 +545,9 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { // Failing tests from issues... - test.skip('issue #116843: indent after arrow function', () => { + test('issue #208215: indent after arrow function', () => { - // https://github.com/microsoft/vscode/issues/116843 + // https://github.com/microsoft/vscode/issues/208215 const model = createTextModel("", languageId, {}); disposables.add(model); @@ -562,11 +562,28 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { 'const add1 = (n) =>', ' ', ].join('\n')); - viewModel.type('n + 1;'); + }); + }); + + 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;', + ' n + 1;', '', ].join('\n')); }); diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 12c05394dd9..16b5bfd131b 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -40,4 +40,8 @@ export const javascriptOnEnterRules = [ beforeText: /^\s+([^{i\s]|i(?!f\b))/, action: { indentAction: IndentAction.Outdent } }, + { + beforeText: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/, + action: { indentAction: IndentAction.Indent } + }, ]; From 997fa7f0d33e94c8a80f5bdd0fe449cdd420ac2f Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 17:57:12 +0100 Subject: [PATCH 21/47] adding one more test --- .../test/browser/indentation.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 83ea2a23d0f..2c987a5bfae 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -565,6 +565,31 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + 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 want to have one outdent pattern corresponding to an 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 From abb6332a8cb313523aa911965038cbf2cad08549 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Wed, 20 Mar 2024 15:50:03 +0100 Subject: [PATCH 22/47] chore: Update codicons --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 79628 -> 79844 bytes src/vs/base/common/codiconsLibrary.ts | 8 ++++++++ 2 files changed, 8 insertions(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 25c7f25a69b3e164b21de8f2e64e46da35da0f5f..57eda48f1db732ce3cff0fccb9e75f57a58a965f 100644 GIT binary patch delta 4015 zcmYk?+T2d-?gDIln*;6+hFfmGB~cG*EzML_md*+c|f@{(GjL7|VD8JU?{ znYn~%A}SSa%9O{`nbR~g9nXxX(}8=enIWfXtmgCU_s^)W=Y4c9DCzeI2iE`f1T#{ z`+6}=5!>G%1+z{fBB-Cz)Wm_Gd4;Z2C)v+ zsFEI8gn9C`{6QW`6ndmxCgGYKl|6{UR7`{iPRS>7O0MB$JSqF|99r=-cA*W=pc|ch zXVPzfQxts z@8UA9;41p@K0d&Q7{W*R47cQ;@-O*O9!jy4NTV#3CMidkG)S}5NxsaMK-f!{?3W>& z=V|el3bDfCi!3~kK)H$%6vGcKn1gUxi}xfJ4$>_*>$ za8}ME1KIGF4fq(J;uE}pBX|)<@e+D*SSCmo{G}H+rB=R>Kg-whC&`dik}jTdN6yIw z$@7(J_v%I?b5mSGR#V!wxC>MJ-XEpLVsFqxN+gR>uE=>>_Dc5)j%LqS` ziuAFS8Wyru8V<148fLOJ8aA-C8u)WY)@#^k47z&H8sw;6!#cJ>gW6N0hAQKcs~PhQ zKeuJ*F$&$xxMp;?nZu&EeusvstVP2_wnKvl`;>-L#+Pm(E-!QRjD{x-cXw~ga~$o{ z(8@lm;c2!@!!GuD4Q*_X2DK;t0#neWb3`m^ zfhio!)i8|>(x49PF%4>m!5Y-I^E9Y!=W9^ghGb%b~}MnfX2wj(65aT-!s^%_Ddt6oD$WBEZ-kj`4v4hXAQwFAOxHd(_O zR_%YxZ)*mWAVvPJ6r7jeZl4vIDGU`sSuSoJOl9jtm6 zgq^H<7lhqxg@!$>dP9W0Y?X#**=h~^RF0}QN;sr`K&^&jta{gkSJ(|2US;bvyvEjR zILWG+A)IE_%n)8@)yxpyU^i-blU1`sIK!%0vT%WOe9)rd0=rqmMYdJLJ8YYVcUd*9 zgv+d&R>BolO)KFlt7etZ&u-W7KKrDG5BQZcs-~OpA**JCFvP0qCVa#`rQtJHO+De3 z(J^%wx(t(VHtdZ&-w>QPI(?T}e7RoDBq57cGf8-!J+AQ^T1M5ZlBfecp`nCTGfXIE z)eIB-*i#zRM5);(%wg4R6TjzWl(QYA7P6KDHQmH7e;HNNO%lt#rSbNzZdyo~4@YXw zi8lr^s^*+zF01C8Fq`euFvA$07Gl}Yk=idotx<1+@HVSvo1lKqHH|kHGCH8idG@-7 z4EBbGZ1$$cn-Cct)I?o|>Z}nyW_fjVo1Pgr%<2rsbetPzf|>P3VXS+yPE zD63vic!^bKme9-I(s0E7(RZ>TrP3Vh{oHU z62I3c8yx*dlVSE}4T}sv|6Q;(PWl(u7l>G0HA!LJG!(J!8gGNdIzba2i*=#~C)Pv5 zan@7g?UGo%G>K;?X{a@tW_qvSiLg%AB$S<^!OTw8pnkHi21j<9h8SaLrr9N#BhHJ0 zI%AB_@FQ!TrJ=$IFo(GCv|58STsKP0-YarAny2yhOsw-Y-l&N+L=#>(R&`7S8#YV> z`g+Y1Y%JeII7Ng<6h?GKSR?Hs3nLFieiP*o6&JNVY9Q)|=&0!C=;P5}NBzlhe_i*k&?hmVjR=2FayT-I;=9;24BY8P_ zpRZlG_F%qKeoX%M{67{X7xWZ7D%`y;d0oxAXNp>iP8SUn{jE5-xViX1@lc6F$&r%& zlCjc-rFo^hOHY?t{!-RlcDn3txlj3>@}DcVR$Q+1t4yriS@~1d!m9SF{;G%7%d0!9 z@70)U(rdo0jo1)TH&*XgA6I{&{;LL4gKtA}LvO>aM&HK%#<3<-lV?*v)54~MO*^{iod5s1D~INz)!~1dk22l> delta 3795 zcmXZed2keE9>($ClLSbRkPtKpkOMN2Gm@YJa+r${5^|86gh0;8A>@J#Ap~W(+a4>p}Xt4EbHn%uRp2!^tXF@-t_Qx_e|gZ z&d+tnZ%2Z2#s$d~2?!P$xxb-lb^WdOufG-@_>}wn z!1aq1+t}>r46Xa+o(Q}mgJPRnYpeV6e&JZ#F%jS0&DEW4K5ne%{&2p3bW3$}o%QeY zS%Qei_}bmJ*7iWh&F?&sf~W+M@S8g?+5&F~_+FYmy?D}2?-bnViNg3nR<8dSQzL%e zCw`&g`|Da9J6L2ug!uWoIVwbg`HGKwtJ~$i=Dy(x@Z@^qt*U?-&fVPWE8k%*Ql$qE zV+Tg#2@FQ76B2Mmjki|i5P)k48vi$Ca=o{yofcj z9WFSq5$mxMoA3-a!zq{0g{|Tvelmcc>pmGGfihHrB-jcc{CB4heBp-x+=n3uL=cAK zeuN<$qYwcb#$h}rU?Sr15GErY37CRun2yIV12ZuTv++2Rkb*f#LptUm6Z4UUY~-K_ z#VEl-lwuJoQH5&Mq8^QCLNi*>hUMtMDs(!r8c*UWti?KPz|+`*ZP*112k{bK!K-); zNANn1;td?b+vvu-IDsCV#(Ox6bGVEv_yB$Q5Z7>B{vrRAf60$hDOJ)cGo(#wv0YkZ zg*3?`87t#3KwPp@uHqe@s4%gM7j+|%hiCDioJR$gAPOrm7LQ69&d3}LlxO9#{12g+ zDiaWkNSP+nB@@r%B;LZCI4f`~rA|D_|qGh!FSBB#hLc}AtWSb0> zLEltjd@G~mBY+pr~u! zB|U7N!ozI6!Vb1UVKm#Q@C5rC@cwWwRlM58y-Z0C+oaITHj6}=1T?%K}H45igGpmHlteI8975@Hs%(N0dV9jh0`dBl?gb&&E3fEXO-Gu9w zYt%+;w}K-IF~G7%#N!=ncf>5`NbY}GA&)h)M|hS!r1)Dc9y609=UFq8gbMbs!V=cZ zDj|wBvq~^CXl9i#mNm0V{KXd!XEnquq>LMHDx9&ZBEvAy>Wqw^63W-iOcEam#B)-~ z1oo6dENf<%5NZ7(GT!+-x6B+8%n0+532(9QE108QP<*Hm&qXEX99~k$WqTF!*~^L# zAL6;9#9VadR1mJReIoT{fseUi&IREk*6bwgVa+ySFKgaM*vFdlMcB_?S9pQFp>Tlx zL}9n(jj|!y3XTrr)6V$l9u!))qURyTiXA;G&`ge7A;fAL{lrChb!>v@!qe%E5^&|F(jSk2{ia*B;GJ3 zN7-=2rzP=@P?Eroq8kKq^^~*V>b6!i^oYtH6{oIndU30IePfXvC-aF54 zUg^BgGGa42GF%xKGkr2=W)@_2Wgf`9H-GZ{b6KHTomt1T?q!e7c4c48{w8N;PDzeE zH!62i?y&_i3to0ExSdy&=gPa9ACW&lzpG$yfxX~L;mE?&!urDPh2IqU6pbzFD*C)Q zxHzx4qxg97?UKnQu9Du8TMNS%9w{AKy0!F9S#H_YvY!@BTePFxuY7*_nZ=1q{FcNl zNv@b&QB~1Vaj@ds%DBqx%C(h!RRvX!s_v>g)gjf1)m7DdYvO90RW+`f-`8%hJ#Bx~ z-e$j9=TkSk?o{2K`iJWs^(X3oY)EZ5(9qlH-*~9;?$WzW-Ay-}er{gcysNpl`STXP zmXelDEuXhKTDx0&Tl-tTY`xQ#+xA>r|MKwV70dgV-(8Wu;?wrV_WkW{M}(u)(e3zt z U#xCEp2V-z@$k~>7bofL54}OCQHvj+t diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a3e87b12ef7..c3528c268b9 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -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; From cb9524cf4b3252559442979f20cb1c1300b297e8 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 10:09:14 -0700 Subject: [PATCH 23/47] Allow separators to have tooltips & descriptions (#208224) This allows the full-length separators to have tooltips and descriptions. (They had support for tooltips already, but my refactoring to a tree broke that) so really this just adds descriptions and fixes tooltips. Because of this, we now adopt the description in favor of the tooltip for QuickSearch --- src/vs/platform/quickinput/browser/quickInputTree.ts | 8 ++++---- src/vs/platform/quickinput/common/quickInput.ts | 1 + .../browser/quickTextSearch/textSearchQuickAccess.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 0b64f90f402..c4dc18c2788 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -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; diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 79224918075..820544bcd69 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -57,6 +57,7 @@ export interface IQuickPickSeparator { type: 'separator'; id?: string; label?: string; + description?: string; ariaLabel?: string; buttons?: readonly IQuickInputButton[]; tooltip?: string | IMarkdownString; diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index 33320b79444..3172a47ff34 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -254,7 +254,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider Date: Wed, 20 Mar 2024 10:09:51 -0700 Subject: [PATCH 24/47] cli: allow specifying a server-data-dir and extensions-dir (#208225) --- cli/src/commands/args.rs | 16 ++++++++++++++++ cli/src/tunnels/code_server.rs | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 11f6e93c6e3..dcdbd808fe7 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -662,12 +662,28 @@ pub struct TunnelServeArgs { /// Requests that extensions be preloaded and installed on connecting servers. #[clap(long)] pub install_extension: Vec, + + /// Specifies the directory that server data is kept in. + #[clap(long)] + pub server_data_dir: Option, + + /// Set the root path for extensions. + #[clap(long)] + pub extensions_dir: Option, } 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()); + } } } diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 592ad121291..bb5a9f8d08f 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -57,6 +57,8 @@ pub struct CodeServerArgs { pub log: Option, pub accept_server_license_terms: bool, pub verbose: bool, + pub server_data_dir: Option, + pub extensions_dir: Option, // extension management pub install_extensions: Vec, pub uninstall_extensions: Vec, @@ -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")); } From 0b391fa2d1b098d03aac3de149b518c3d865583a Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 18:27:07 +0100 Subject: [PATCH 25/47] adding todo --- .../editor/test/common/modes/supports/javascriptOnEnterRules.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 16b5bfd131b..b7324b2397b 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -41,6 +41,7 @@ export const javascriptOnEnterRules = [ action: { indentAction: IndentAction.Outdent } }, { + // TODO: Do not leave this here - place instead in the indent regex pattern if we use this method beforeText: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/, action: { indentAction: IndentAction.Indent } }, From 3d71904882c468817ea50418f3a4d9090ca6383a Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 18:31:26 +0100 Subject: [PATCH 26/47] adding only tests --- extensions/typescript-basics/language-configuration.json | 8 +------- .../contrib/indentation/test/browser/indentation.test.ts | 3 ++- .../test/common/modes/supports/javascriptOnEnterRules.ts | 5 ----- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index d2652244965..2d14b1cab19 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -215,12 +215,6 @@ "action": { "indent": "outdent" } - }, - { - "beforeText": "^\\s*(var|const|let)\\s+\\w+\\s*=\\s*\\(.*\\)\\s*=>\\s*$", - "action": { - "indent": "indent" - } - }, + } ] } diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 2c987a5bfae..ec54084f5d3 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -548,6 +548,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { test('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); @@ -568,7 +569,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { 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 want to have one outdent pattern corresponding to an indent pattern, and not a generic outdent and indent pattern + // We could use one outdent pattern corresponding per indent pattern, and not a generic outdent and indent pattern const model = createTextModel([ 'const add1 = (n) =>', diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index b7324b2397b..12c05394dd9 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -40,9 +40,4 @@ export const javascriptOnEnterRules = [ beforeText: /^\s+([^{i\s]|i(?!f\b))/, action: { indentAction: IndentAction.Outdent } }, - { - // TODO: Do not leave this here - place instead in the indent regex pattern if we use this method - beforeText: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/, - action: { indentAction: IndentAction.Indent } - }, ]; From fe900ca88f5187eac53b743cd3dc76c4540cc9c2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 18:32:06 +0100 Subject: [PATCH 27/47] skipping additional test --- .../editor/contrib/indentation/test/browser/indentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index ec54084f5d3..84a06ae89be 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -545,7 +545,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { // Failing tests from issues... - test('issue #208215: indent after arrow function', () => { + 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*$/ From 4b4af504a9a78d1a063de742aa40e9d611bbbe54 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 18:38:42 +0100 Subject: [PATCH 28/47] adding this potential regex pattern --- .../editor/contrib/indentation/test/browser/indentation.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 84a06ae89be..89e080181ed 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -675,6 +675,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { 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() {', From 5e14e2e569cd728fd69bda7e79a340fa6ceba816 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 20 Mar 2024 18:57:31 +0100 Subject: [PATCH 29/47] adding one more test for the incorrect indentation within comments --- .../test/browser/indentation.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 6b78736694f..9ca3ac32414 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -666,6 +666,31 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + 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 From c37edea794df789b04694e791a40aa2ee412cb0e Mon Sep 17 00:00:00 2001 From: Adam Byrd Date: Wed, 20 Mar 2024 11:24:59 -0700 Subject: [PATCH 30/47] Implement separate colors for primary and secondary cursors when multiple cursors are present (#181991) * Add support for separate primary cursor color when multiple cursors are present - Does not change the existing behavior when there's a single cursor. editorCursor.foreground and background are still used. - Add editorCursor.multiple.primary.foreground and background theme colors for the primary cursor. Only used when multiple cursors exist. Fallback to editorCursor.foreground/background when theme colors aren't set. - Add editorCursor.multiple.secondary.foreground and `background theme colors for non-primary cursors. Only used when multiple cursors exist. Fallback to editorCursor.foreground/background when theme colors aren't set. Add cursor-primary and cursor-secondary html classes to target with cursor color styles. No new class is introduced in the single-cursor case. - Currently does not affect overview ruler colors. editorCursor.foreground is still used, even when multiple cursors are present. * Update overview ruler to use primary and secondary cursor colors - This maintains the existing handling for colors being undefined. However, each of these colors have defaults do I'm not sure if it's actually possible for them to be undefined * Fix formatting * Fix compilation errors * Fall back to the existing cursor colors (to avoid breaking existing themes) --------- Co-authored-by: Alex Dima --- .../overviewRuler/decorationsOverviewRuler.ts | 59 +++++++++++++------ .../viewParts/viewCursors/viewCursor.ts | 30 +++++++++- .../viewParts/viewCursors/viewCursors.ts | 49 ++++++++++----- .../editor/common/core/editorColorRegistry.ts | 4 ++ 4 files changed, 108 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 1338656acab..0ca31065a45 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -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; 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) { diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index 36506e9fed0..4502698cfc5 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -35,6 +35,12 @@ class ViewCursorRenderData { ) { } } +export enum CursorPlurality { + Single, + MultiPrimary, + MultiSecondary, +} + export class ViewCursor { private readonly _context: ViewContext; private readonly _domNode: FastDomNode; @@ -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); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index 1be969b7497..ceb27ce5ea3 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -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}; }`); + } } } - }); diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 95b738b7b6f..88e0419268f 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -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.')); From fb14b695a7d08735f6cea325bc5339d6354cb2e6 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 20 Mar 2024 11:52:20 -0700 Subject: [PATCH 31/47] Fix accidently showing chat panel welcome (#208237) * Fix accidently showing chat panel welcome * Update distro commit ID --- package.json | 2 +- .../browser/accountsEntitlements.contribution.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 25ab5b12dc6..a3c0ed27175 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd", + "distro": "4339a6a9b230ccc77f864167a18bb70e4e662aae", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index 5acfa3942f1..4975185248d 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -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(accountsBadgeConfigKey).value ?? false; const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; From d6474a05607308e870411d65e89b218ebd9fd0bb Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 12:23:04 -0700 Subject: [PATCH 32/47] Couple of styling updates to quick pick (#208242) 1. Separator descriptions no longer include opacity so that contrast ratio is satisfied 2. The separators are now a little taller so that they stand out compared to regular items --- .../quickinput/browser/media/quickInput.css | 15 ++++++++++++--- .../platform/quickinput/browser/quickInputTree.ts | 5 ++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 583f49ae26a..3afa06e5888 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -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 { @@ -323,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; diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index c4dc18c2788..aa4329a2867 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -277,9 +277,8 @@ class QuickPickSeparatorElement extends BaseQuickPickItemElement { class QuickInputItemDelegate implements IListVirtualDelegate { getHeight(element: IQuickPickElement): number { - if (!element.item) { - // must be a separator - return 24; + if (element instanceof QuickPickSeparatorElement) { + return 30; } return element.saneDetail ? 44 : 22; } From 6efb7ca55b721dedefd3e6d339f24d63b4481660 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 20 Mar 2024 15:45:38 +0100 Subject: [PATCH 33/47] rename suggestions: refactor: formatting --- src/vs/editor/contrib/rename/browser/rename.ts | 10 +++++++++- .../editor/contrib/rename/browser/renameInputField.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 8daef7eff27..8dcbcf0222a 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -245,7 +245,15 @@ class RenameController implements IEditorContribution { trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(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, + selectionStart, + selectionEnd, + supportPreview, + newSymbolNameProvidersResults, + renameCandidatesCts + ); trace('received response from rename input field'); if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index af5f430254b..d4b65af3c02 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -346,7 +346,15 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis } } - getInput(where: IRange, currentName: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise { + getInput( + where: IRange, + currentName: string, + selectionStart: number, + selectionEnd: number, + supportPreview: boolean, + candidates: ProviderResult[], + cts: CancellationTokenSource + ): Promise { this._isEditingRenameCandidate = false; From 67a3dd0cc277457214e926b1d6b6a669f53f233a Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 20 Mar 2024 16:04:49 +0100 Subject: [PATCH 34/47] rename suggestions: refactor: move selection computation where it belongs --- .../editor/contrib/rename/browser/rename.ts | 11 ------- .../rename/browser/renameInputField.ts | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 8dcbcf0222a..ceed1212b81 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -234,22 +234,11 @@ class RenameController implements IEditorContribution { 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; - } - trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); const inputFieldResult = await this._renameInputField.getInput( loc.range, loc.text, - selectionStart, - selectionEnd, supportPreview, newSymbolNameProvidersResults, renameCandidatesCts diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index d4b65af3c02..8aa19eb1ad0 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -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[], cts: CancellationTokenSource): Promise; + getInput( + where: IRange, + currentName: string, + supportPreview: boolean, + candidates: ProviderResult[], + cts: CancellationTokenSource + ): Promise; acceptInput(wantsPreview: boolean): void; cancelInput(focusEditor: boolean, caller: string): void; @@ -349,13 +355,13 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis getInput( where: IRange, currentName: string, - selectionStart: number, - selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource ): Promise { + const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName); + this._isEditingRenameCandidate = false; this._domNode!.classList.toggle('preview', supportPreview); @@ -441,6 +447,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); From fab256d903d08f0678d59ad026962de31645c626 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 20 Mar 2024 17:45:57 +0100 Subject: [PATCH 35/47] rename suggestions: refactor: rename 'rename widget' accordingly --- src/vs/editor/contrib/rename/browser/rename.ts | 6 +++--- src/vs/editor/contrib/rename/browser/renameInputField.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index ceed1212b81..322a42dc18a 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -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.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 { diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index 8aa19eb1ad0..036f03db50d 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -100,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; From 52f886230467594878bb783c21b897a9ef3237a6 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 20 Mar 2024 18:42:05 +0100 Subject: [PATCH 36/47] rename suggestions: cancel rename suggestion providers if user started typing --- .../editor/contrib/rename/browser/rename.ts | 9 ++++---- .../rename/browser/renameInputField.ts | 21 +++++++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 322a42dc18a..dba920953ba 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -229,10 +229,9 @@ 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 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(this.editor.getModel().uri, 'editor.rename.enablePreview'); @@ -240,8 +239,8 @@ class RenameController implements IEditorContribution { loc.range, loc.text, supportPreview, - newSymbolNameProvidersResults, - renameCandidatesCts + requestRenameSuggestions, + cts2 ); trace('received response from rename input field'); diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index 036f03db50d..69c3176bf8c 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -89,7 +89,7 @@ interface IRenameInputField { where: IRange, currentName: string, supportPreview: boolean, - candidates: ProviderResult[], + requestRenameSuggestions: (cts: CancellationToken) => ProviderResult[], cts: CancellationTokenSource ): Promise; @@ -133,6 +133,8 @@ export class RenameWidget implements IRenameInputField, IContentWidget, IDisposa */ private _timeBeforeFirstInputFieldEdit: number | undefined; + private _renameCandidateProvidersCts: CancellationTokenSource | undefined; + private readonly _visibleContextKey: IContextKey; private readonly _disposables = new DisposableStore(); @@ -200,6 +202,9 @@ export class RenameWidget implements IRenameInputField, IContentWidget, IDisposa this._isEditingRenameCandidate = true; } this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed(); + if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) { + this._renameCandidateProvidersCts.cancel(); + } this._renameCandidateListView?.clearFocus(); }) ); @@ -356,12 +361,16 @@ export class RenameWidget implements IRenameInputField, IContentWidget, IDisposa where: IRange, currentName: string, supportPreview: boolean, - candidates: ProviderResult[], + requestRenameSuggestions: (cts: CancellationToken) => ProviderResult[], cts: CancellationTokenSource ): Promise { 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; this._domNode!.classList.toggle('preview', supportPreview); @@ -379,8 +388,12 @@ export class RenameWidget implements IRenameInputField, IContentWidget, IDisposa 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(); From 92592894f724df264d4215d94a4f517b32021f1a Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 20 Mar 2024 19:58:15 +0100 Subject: [PATCH 37/47] rename suggestions: refactor: don't use any & change trace prefix --- src/vs/editor/contrib/rename/browser/renameInputField.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index 69c3176bf8c..eb6bfde534b 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -553,8 +553,8 @@ export class RenameWidget implements IRenameInputField, IContentWidget, IDisposa 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); } } From abd11f059e2cc628201f34caf5a5e3526ed34990 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 20 Mar 2024 17:16:30 -0300 Subject: [PATCH 38/47] Fix command buttons in 'push' (#208248) Fix #207885 --- src/vs/workbench/api/common/extHostChatAgents2.ts | 2 +- src/vs/workbench/api/common/extHostChatVariables.ts | 9 +++++++-- src/vs/workbench/api/common/extHostTypeConverters.ts | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 7a4c69dc8f3..3c39b7eccf8 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -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; }, diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index e56e51676d4..26fac7e95e0 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -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; } }; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 7eba744d030..bcaf13466d7 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -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', From 5af7860ca23d38281f1f6d5b229b4668d2f13e24 Mon Sep 17 00:00:00 2001 From: VLADIMIR VATSURIN Date: Wed, 20 Mar 2024 16:19:26 -0400 Subject: [PATCH 39/47] Fix file relative path link (#181475) * Fix for file link with relative path * Removed redundant comments * Simplify fix to accomodate Windows paths --------- Co-authored-by: Sidebail Co-authored-by: Alexandru Dima --- src/vs/editor/contrib/links/browser/links.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index 9527453bf32..073c85c85ea 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -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)}`; } From 06f1c12edb14b1db1ead2250e52f6c51dd7eeba3 Mon Sep 17 00:00:00 2001 From: Fizz <13631686641@sina.cn> Date: Thu, 21 Mar 2024 05:09:24 +0800 Subject: [PATCH 40/47] Update IActionDescriptor.precondition desc (#176124) * Update standaloneCodeEditor.ts * Update comment and monaco.d.ts --------- Co-authored-by: Alexandru Dima --- src/vs/editor/standalone/browser/standaloneCodeEditor.ts | 2 +- src/vs/monaco.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 05305694e57..c80bdf63686 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -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; /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 509f549d41b..ae56ff86935 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -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; /** From 288be7d337b66d5fc0ef2ecbf06238e84653992e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 20 Mar 2024 14:21:55 -0700 Subject: [PATCH 41/47] Fix settings keys (#208253) Fixes #207804 --- .../markdown-language-features/package.nls.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 2307358b749..a4b25260ba8 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -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.", From 78d4f34a94e8c24f883fce5cad180a4f61009a4c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 15:15:03 -0700 Subject: [PATCH 42/47] Register toggle since it's a disposable (#208249) Saw this in our unit tests when I was playing with depending on Checkbox. --- src/vs/base/browser/ui/toggle/toggle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 22d60e98508..9e70dbdbe6f 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -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; From 7eb5c6cff32ce4c0cbbb36d77964e622e1a2780a Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Wed, 20 Mar 2024 15:45:50 -0700 Subject: [PATCH 43/47] cell diagnostic actions (#208261) * add command to open cell error diagnostic quickfix menu * add keybinding, filter actions * context key for has cell diagnostics * clear diagnostics on content change --- .../contrib/cellCommands/cellCommands.ts | 48 ++++++++++++++++++- .../cellDiagnostics/cellDiagnostics.ts | 4 +- .../browser/view/cellParts/cellContextKeys.ts | 8 +++- .../browser/viewModel/codeCellViewModel.ts | 6 ++- .../notebook/common/notebookContextKeys.ts | 1 + 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts index 77a91525426..2027ddaa5b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -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 { + 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) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts index 154a590812b..054e1fda1a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts @@ -70,7 +70,9 @@ export class CellDiagnostics extends Disposable { } public clear() { - this.clearDiagnostics(); + if (this.ErrorDetails) { + this.clearDiagnostics(); + } } private clearDiagnostics() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index 7bc1ba28b3a..f3571422003 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -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; private cellGeneratedByChat!: IContextKey; + private cellHasErrorDiagnostics!: IContextKey; private markdownEditMode!: IContextKey; @@ -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() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index f2ecbf4d4fc..517b47d7fe7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -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 }); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 8345a520e5c..a487c2c0fda 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -47,6 +47,7 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('noteboo export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey('notebookCellGenerateByChat', false); +export const NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS = new RawContextKey('notebookCellHasErrorDiagnostics', false); // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); From a49fc50d7db0ee121e360f07bbb63049438668a4 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Mar 2024 16:16:44 -0700 Subject: [PATCH 44/47] Bump distro (#208264) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3c0ed27175..aaad3cf8140 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "4339a6a9b230ccc77f864167a18bb70e4e662aae", + "distro": "7734ec27c8ec09ddc68bc3618e17d9a8b40fbfd9", "author": { "name": "Microsoft Corporation" }, From 042c0893d9d44d79626d73d0f23e268fc78eab45 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 20 Mar 2024 20:44:03 -0300 Subject: [PATCH 45/47] Handle duplicate chat participant names (#208142) * Enable duplicate chat participant names #208103 * Register participants with an ID * Update participant history in API * Changes to dupe chat suggest widget, and fix serialize/deserialize * Tweaks * Test fixes * Fix tests * Test fixes * Fix integration test --- extensions/vscode-api-tests/package.json | 1 + .../src/singlefolder-tests/chat.test.ts | 14 +- .../api/browser/mainThreadChatAgents2.ts | 42 +++--- .../workbench/api/common/extHost.api.impl.ts | 8 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostChatAgents2.ts | 30 ++-- .../api/common/extHostTypeConverters.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 4 +- .../chat/browser/actions/chatActions.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 4 +- src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../browser/chatContributionServiceImpl.ts | 75 ++++++---- .../contrib/chat/browser/chatListRenderer.ts | 2 +- .../chatMarkdownDecorationsRenderer.ts | 7 +- .../contrib/chat/browser/chatWidget.ts | 13 +- .../browser/contrib/chatInputEditorContrib.ts | 93 ++++++++---- .../contrib/chat/common/chatAgents.ts | 138 +++++++++--------- .../chat/common/chatContributionService.ts | 5 +- .../contrib/chat/common/chatModel.ts | 16 +- .../contrib/chat/common/chatParserTypes.ts | 15 +- .../contrib/chat/common/chatRequestParser.ts | 43 +++--- .../contrib/chat/common/chatService.ts | 5 +- .../contrib/chat/common/chatServiceImpl.ts | 6 +- .../chat/test/browser/chatVariables.test.ts | 2 +- ..._agent_and_subcommand_after_newline.0.snap | 6 + ..._subcommand_with_leading_whitespace.0.snap | 6 + ...uestParser_agent_with_question_mark.0.snap | 6 + ...er_agent_with_subcommand_after_text.0.snap | 6 + ...hatRequestParser_agents__subCommand.0.snap | 6 + ..._agents_and_variables_and_multiline.0.snap | 6 + ..._and_variables_and_multiline__part2.0.snap | 6 + .../ChatService_can_deserialize.0.snap | 96 ++++++++++++ .../ChatService_can_serialize.0.snap | 9 ++ .../ChatService_can_serialize.1.snap | 102 +++++++++++++ .../chat/test/common/chatModel.test.ts | 2 +- .../test/common/chatRequestParser.test.ts | 22 +-- .../chat/test/common/chatService.test.ts | 23 ++- .../common/mockChatContributionService.ts | 1 - .../chat/test/common/voiceChat.test.ts | 14 +- .../test/browser/inlineChatController.test.ts | 33 ++--- .../chat/browser/terminalChatController.ts | 34 +++-- .../vscode.proposed.chatParticipant.d.ts | 23 ++- ...ode.proposed.chatParticipantAdditions.d.ts | 12 +- 43 files changed, 645 insertions(+), 298 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5ea2340b270..1c54b960a91 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -65,6 +65,7 @@ "contributes": { "chatParticipants": [ { + "id": "api-test.participant", "name": "participant", "description": "test", "isDefault": true, diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 1a07b475074..f2e28006ced 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -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 => { 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(); - 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; diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 631f51548dc..0aaaee8cf14 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -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 { @@ -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; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 53b6cb09800..548f883ae97 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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 diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 75fd8f80b89..3edd6280047 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1209,7 +1209,7 @@ export interface IExtensionChatAgentMetadata extends Dto { } 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; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 3c39b7eccf8..2c5820cc2d6 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -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; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index bcaf13466d7..3f3c098dc45 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2548,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 { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 84c1dd595ba..3b7b25329bd 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -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, readonly result: vscode.ChatResult, - readonly participant: { extensionId: string; name: string }, + readonly participant: string, readonly command?: string ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 929fb17898c..6d9c2d051df 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -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}`); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 33047b3b5b5..ff0d0ebc145 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -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)); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index f4efe913e16..4baec7951e6 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -112,6 +112,7 @@ export interface IChatWidget { readonly providerId: string; readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; + lastSelectedAgent: IChatAgentData | undefined; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 2b358ca4519..963b28b0f5d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -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({ 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(); + private _participantRegistrationDisposables = new DisposableMap(); 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(); - private _registeredParticipants = new Map(); 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()); - } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 11375d74149..bc48f4aaf59 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -359,7 +359,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer 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 { } } } - diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index e0f24df80d4..dfde70080b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -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.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), { selectedAgent: this._lastSelectedAgent }); } return this.parsedChatRequest; @@ -212,6 +212,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; } @@ -655,7 +664,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); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, { selectedAgent: this._lastSelectedAgent }); if (result) { const inputState = this.collectInputState(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index eff2859569b..6f15695df6f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -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) { @@ -347,13 +350,18 @@ class AgentCompletions extends Disposable { const agents = this.chatAgentService.getAgents() .filter(a => !a.isDefault); return { - 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 { - 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 }; }) @@ -431,31 +439,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; }))) }; @@ -465,6 +479,32 @@ class AgentCompletions extends Disposable { } Registry.as(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 @@ -592,12 +632,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 @@ -605,7 +647,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); @@ -625,6 +667,7 @@ class ChatTokenDeleter extends Disposable { } previousInputValue = this.widget.inputEditor.getValue(); + previousSelectedAgent = this.widget.lastSelectedAgent; })); } } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index d810ee64f8d..4825be6757f 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -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('chatAgentService'); +interface IChatAgentEntry { + data: IChatAgentData; + impl?: IChatAgentImplementation; +} + export interface IChatAgentService { _serviceBrand: undefined; /** * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - 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; + invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getAgents(): IChatAgentData[]; - getRegisteredAgents(): Array; - getActivatedAgents(): Array; getAgent(id: string): IChatAgentData | undefined; + getAgents(): IChatAgentData[]; + getActivatedAgents(): Array; + 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(); + private _agents: IChatAgentEntry[] = []; - private readonly _onDidChangeAgents = this._register(new Emitter()); + private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = 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)); @@ -212,35 +228,19 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getRegisteredAgents(): Array { - 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[] { @@ -249,12 +249,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 { - const data = this._agents.get(id); + const data = this._getAgentEntry(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); } @@ -263,7 +263,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._agents.get(id); + const data = this._getAgentEntry(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); } @@ -283,6 +283,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; } diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index b0e4facf10c..aaef3c6aacb 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -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; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 1cd78337dab..9aeb6939946 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -591,7 +591,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(raw.agent) : undefined; + this.reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? @@ -613,6 +613,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)]; @@ -703,7 +713,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); } @@ -802,7 +812,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, diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 59f8b873cbb..7edda68e10e 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -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( diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index 52c683f60a4..59db1b487ac 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -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 { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { 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,18 +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): IParsedChatRequest { + parseChatRequest(sessionId: string, message: string, 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)!; let lineNumber = 1; let column = 1; @@ -40,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); + newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, 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) { @@ -89,17 +90,23 @@ export class ChatRequestParser { }; } - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { - const nextVariableMatch = message.match(agentReg); - if (!nextVariableMatch) { + private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, 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) { return; } @@ -121,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): ChatRequestAgentPart | ChatRequestVariablePart | undefined { @@ -142,7 +149,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index cd113c0dc7b..55216277573 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -15,6 +15,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { 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; kind: 'agentDetection'; } @@ -283,7 +284,7 @@ export interface IChatService { /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean): Promise; + sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, parserContext?: IChatParserContext): Promise; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 022800fbfc7..ea1d8f1ec79 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentServi 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): Promise { + async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, parserContext?: IChatParserContext): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); if (!request.trim()) { this.trace('sendRequest', 'Rejected empty message'); @@ -458,7 +458,7 @@ export class ChatService extends Disposable implements IChatService { return; } - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request); + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, parserContext); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? this.chatAgentService.getDefaultAgent()!; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 35173312a8c..6d6856156ee 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -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 () { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index ae1818689d0..53316feaeda 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -27,6 +27,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index 190c3555054..334ce29b40c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -27,6 +27,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 65d00490273..47294116f2a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 6585ff1e0e5..511527e4225 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index fc2a622c9ac..8da51e002ce 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 6f9eaa531cf..6521b9c0ff6 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index fee5731f3e3..62e78ffff91 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap new file mode 100644 index 00000000000..f474e2a2c47 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -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" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap new file mode 100644 index 00000000000..75c5fa71f40 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap @@ -0,0 +1,9 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap new file mode 100644 index 00000000000..5a6706f49aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -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" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index ffcda81f6de..3d63e2ed9d3 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -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 () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index b1d4231aeb2..1c7e876ba64 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -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()({}); varService.getDynamicVariables.returns([]); @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return { id: 'agent', metadata: { description: '' }, slashCommands }; + return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [], metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - 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()({}); - 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()({}); - 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()({}); - 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()({}); - 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()({}); - 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()({}); - 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()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 64111ef5f30..3125cf09d88 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -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 diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts index e1adddb2dec..27a687cb24d 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -9,7 +9,6 @@ export class MockChatContributionService implements IChatContributionService { _serviceBrand: undefined; constructor( - public readonly registeredParticipants: IChatParticipantContribution[] = [] ) { } registeredProviders: IChatProviderContribution[] = []; diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c993bae97ea..97e11f717ea 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -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 { 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 { throw new Error(); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getRegisteredAgents(): Array { 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 { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 276c86cb858..52a148b3600 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -7,16 +7,20 @@ import * as assert from 'assert'; import { equals } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; 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'; @@ -30,24 +34,17 @@ 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 } 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, IInlineChatEditResponse, 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 { MarkdownString } from 'vs/base/common/htmlContent'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -117,8 +114,6 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], - [IChatContributionService, new MockChatContributionService( - [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, inlineChatService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], @@ -152,15 +147,7 @@ suite('InteractiveChatController', function () { }] ); - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - const chatAgentService = instaService.get(IChatAgentService); - const agent = { - async invoke(request, progress, history, token) { - return {}; - }, - } satisfies IChatAgentImplementation; - store.add(chatAgentService.registerAgent('testAgent', agent)); - + instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index d1169e3d986..3dad501a1df 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -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 = 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; @@ -285,13 +291,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); diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index acdf2320b60..28624132313 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -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, result: ChatResult, participant: { extensionId: string; name: string }); + private constructor(response: ReadonlyArray, 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. @@ -446,12 +446,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; } /** diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 3319ce9ca8a..2198ae4d90e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -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; } - - 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; - } } From 09d5f4efc5089ce2fc5c8f6aeb51d728d7f4e758 Mon Sep 17 00:00:00 2001 From: Anthony Stewart <150152+a-stewart@users.noreply.github.com> Date: Thu, 21 Mar 2024 01:17:17 +0100 Subject: [PATCH 46/47] Stop the cursor from jumping when changing prefix in QuickAccess - v2 (#204702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "Stop the cursor from jumping when changing prefix in QuickAcc…" This reverts commit 348e88dc0099fcedf9578013692ce6ca761f3501. * Only keep the selection if the value is unchanged --------- Co-authored-by: Tyler James Leonhardt --- src/vs/base/browser/ui/inputbox/inputBox.ts | 12 +++++++++++ .../quickinput/browser/quickAccess.ts | 9 +++++++++ .../platform/quickinput/browser/quickInput.ts | 20 +++++++++++++++++-- .../quickinput/browser/quickInputBox.ts | 4 ++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 0a06ece485f..b899215c98f 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -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'); } diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index cb35e451aa1..88169c32ed5 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -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; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 3618afe711b..0cce0ef4228 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -723,7 +723,15 @@ export class QuickPick 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(); diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts index b3c6694387c..9ee1440a91a 100644 --- a/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -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(); } From cefbd4fbe7acfd0315c022e1082d4285a8ae6966 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:07:42 -0700 Subject: [PATCH 47/47] show preview label on code actions (#208252) * show proper label on hovers * absolute imports --- .../browser/codeActionController.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 5511dde958b..784be6bebee 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -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) {