diff --git a/extensions/copilot/src/extension/prompt/node/intentDetector.tsx b/extensions/copilot/src/extension/prompt/node/intentDetector.tsx index cdb66eb6bf4..61a58119d02 100644 --- a/extensions/copilot/src/extension/prompt/node/intentDetector.tsx +++ b/extensions/copilot/src/extension/prompt/node/intentDetector.tsx @@ -17,6 +17,7 @@ import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEdi import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { isNotebookCellOrNotebookChatInput } from '../../../util/common/notebooks'; +import { isFalsyOrEmpty } from '../../../util/vs/base/common/arrays'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { Position, Range } from '../../../vscodeTypes'; import { getAgentForIntent, GITHUB_PLATFORM_AGENT, Intent } from '../../common/constants'; @@ -181,6 +182,11 @@ export class IntentDetector implements ChatParticipantDetectionProvider { this.logService.trace('Building intent detector'); + if (builtinParticipants.length === 0 && (isFalsyOrEmpty(thirdPartyParticipants))) { + this.logService.trace('No participants available for intent detection'); + return undefined; + } + const endpoint = await this.endpointProvider.getChatEndpoint('copilot-fast'); const preferredIntent = await this.getPreferredIntent(location, documentContext, history, messageText); diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt new file mode 100644 index 00000000000..2c5d6810026 --- /dev/null +++ b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +console.log("foobar"); + +// one +// two +// three + +logwo + +// * merge/re-use modified file entries +// * no accept/disacrd on new session +// * accept all hunks + +export function fib(nth) { + if (nth <= 0) return 0; + if (nth === 1) return 0; + if (nth === 2) return 1; + return fib(nth - 1) + fib(nth - 2); +} + +///-///-////--- + +const r = /hello/gim; + +export function sum(a, b) { + return a + b; +} + +export function sub(a, b) { + return a - b; +} + +export function sumArray(a) { + return a.reduce((sum, num) => sum + num, 0); +} + +export function div(a, b) { + // console.log fff fff + return a / b; +} + + + +export function mul(a, b) { + return a * b; +} + +export function sumThreeFloats(a, b, c) { + return a + b + c; +} + +/** + * Checks if a given number is a prime number. + * + * @param {number} number - The number to check for primality. + * @returns {boolean} - Returns true if the number is prime, otherwise false. + */ +export function isPrime(number) { + if (number <= 1) return false; + for (let i = 2; i <= Math.sqrt(number); i++) { + if (number % i === 0) { + return false; + } + } + return true; +} + + +export function isEven(n) { + return n % 2 === 0; +} diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected new file mode 100644 index 00000000000..e897e087b1d --- /dev/null +++ b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +console.log("foobar"); + +// one +// two +// three + +logwo + +// * merge/re-use modified file entries +// * no accept/disacrd on new session +// * accept all hunks + +export function fib(nth) { + if (nth <= 0) return 0; + if (nth === 1) return 0; + if (nth === 2) return 1; + return fib(nth - 1) + fib(nth - 2); +} + +///-///-////--- + +const r = /hello/gim; + +export function sum(a, b) { + return a + b; +} + +export function sub(a, b) { + return a - b; +} + +export function sumArray(a) { + return a.reduce((sum, num) => sum + num, 0); +} + +export function div(A, b) { + // console.log fff fff + return A / b; +} + +export function mul(a, b) { + return a * b; +} + +export function sumThreeFloats(a, b, c) { + return a + b + c; +} + +/** + * Checks if a given number is a prime number. + * + * @param {number} number - The number to check for primality. + * @returns {boolean} - Returns true if the number is prime, otherwise false. + */ +export function isPrime(number) { + if (number <= 1) return false; + for (let i = 2; i <= Math.sqrt(number); i++) { + if (number % i === 0) { + return false; + } + } + return true; +} + + +export function isEven(n) { + return n % 2 === 0; +} diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx b/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx new file mode 100644 index 00000000000..49dc18021e1 --- /dev/null +++ b/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { beforeEach, expect, it, suite } from 'vitest'; +import { ITestingServicesAccessor } from '../../../../../platform/test/node/services'; +import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService'; +import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService'; +import { ChatResponseStreamImpl } from '../../../../../util/common/chatResponseStreamImpl'; +import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument'; +import { CancellationToken } from '../../../../../util/vs/base/common/cancellation'; +import { assertType } from '../../../../../util/vs/base/common/types'; +import { URI } from '../../../../../util/vs/base/common/uri'; +import { SyncDescriptor } from '../../../../../util/vs/platform/instantiation/common/descriptors'; +import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation'; +import { ChatResponseTextEditPart } from '../../../../../vscodeTypes'; +import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection'; +import { WorkingCopyOriginalDocument } from '../../../../prompts/node/inline/workingCopies'; +import { createExtensionUnitTestingServices } from '../../../../test/node/services'; +import { IReplaceStringToolParams, ReplaceStringTool } from '../../../node/replaceStringTool'; + + +suite('ReplaceString Tool', () => { + + let accessor: ITestingServicesAccessor; + + const path = join(__dirname, 'fixtures/math.js.txt'); + const fileTsUri = URI.file(path); + + beforeEach(function () { + const services = createExtensionUnitTestingServices(); + + const content = String(readFileSync(path)); + + const testDoc = createTextDocumentData(fileTsUri, content, 'ts').document; + services.define(IWorkspaceService, new SyncDescriptor( + TestWorkspaceService, [[fileTsUri], [testDoc]] + )); + + accessor = services.createTestingAccessor(); + }); + + it('whitespace change everywhere', async () => { + + const input: IReplaceStringToolParams = JSON.parse(`{ + "filePath": "${path.replaceAll('\\', '\\\\')}", + "oldString": "export function div(a, b) {\\n // console.log fff fff\\n return a / b;\\n}", + "newString": "export function div(A, b) {\\n // console.log fff fff\\n return A / b;\\n}" +}`); + + const tool = accessor.get(IInstantiationService).createInstance(ReplaceStringTool); + + expect(tool).toBeDefined(); + + const document = accessor.get(IWorkspaceService).textDocuments.find(doc => doc.uri.toString() === fileTsUri.toString()); + assertType(document); + + const workingCopyDocument = new WorkingCopyOriginalDocument(document.getText()); + + expect(document.getText().includes(input.oldString)).toBe(false); // TAB vs SPACES + + let seenEdits = 0; + + const stream = new ChatResponseStreamImpl((part) => { + + if (part instanceof ChatResponseTextEditPart) { + const offsetEdits = workingCopyDocument.transformer.toOffsetEdit(part.edits); + + if (!workingCopyDocument.isNoop(offsetEdits)) { + seenEdits++; + workingCopyDocument.applyOffsetEdits(offsetEdits); + } + } + + }, () => { }, () => { }); + + const input2 = await tool.resolveInput(input, { + history: [], + stream, + query: 'change a to A', + chatVariables: new ChatVariablesCollection([]), + }); + + await tool.invoke({ input: input2, toolInvocationToken: undefined }, CancellationToken.None); + + expect(seenEdits).toBe(1); + expect(workingCopyDocument.text).toMatchFileSnapshot('fixtures/math.js.txt.expected'); + + }); +});