Path: blob/main/extensions/copilot/src/extension/onboardDebug/node/copilotDebugCommandSessionFactory.tsx
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';7import { IExtensionsService } from '../../../platform/extensions/common/extensionsService';8import { IPackageJson } from '../../../platform/extensions/common/packageJson';9import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';10import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';11import { equals } from '../../../util/vs/base/common/arrays';12import { CancellationToken } from '../../../util/vs/base/common/cancellation';13import { URI, UriComponents } from '../../../util/vs/base/common/uri';14import { ICommandInteractor, ILaunchConfigService } from '../common/launchConfigService';15import { IDebugCommandToConfigConverter } from './commandToConfigConverter';16import { IStartOptions, StartResult, StartResultKind } from './copilotDebugWorker/shared';17import { IStartDebuggingParsedResponse } from './parseLaunchConfigFromResponse';1819const STORAGE_KEY = 'copilot-chat.terminalToDebugging.configs';20const LRU_SIZE = 30;2122interface IStoredData {23cwd: string;24folder: UriComponents | undefined;25args: readonly string[];26inputs: [string, string][];27config: IStartDebuggingParsedResponse;28}2930// Just some random strings that will lead to defined return results if found in the arguments.31const testsStatuses: Record<string, StartResult> = {32'73687c45-cancelled': {33kind: StartResultKind.Cancelled,34},35'73687c45-extension': {36kind: StartResultKind.NeedExtension,37debugType: 'node',38},39'73687c45-noconfig': {40kind: StartResultKind.NoConfig,41text: 'No config generated',42},43'73687c45-ok': {44kind: StartResultKind.Ok,45folder: undefined,46config: { type: 'node', name: 'Generated Node Launch', request: 'launch', program: '${workspaceFolder}/app.js' },47}48};4950export class CopilotDebugCommandSessionFactory {51constructor(52private readonly interactor: ICommandInteractor,53@ITelemetryService private readonly telemetry: ITelemetryService,54@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,55@IDebugCommandToConfigConverter private readonly commandToConfig: IDebugCommandToConfigConverter,56@IExtensionsService private readonly extensionsService: IExtensionsService,57@IWorkspaceService private readonly workspaceService: IWorkspaceService,58@ILaunchConfigService private readonly launchConfigService: ILaunchConfigService,59) { }6061public async start({ args, cwd, forceNew, printOnly, save }: IStartOptions, token: CancellationToken): Promise<StartResult> {62/* __GDPR__63"onboardDebug.commandExecuted" : {64"owner": "connor4312",65"comment": "Reports usages of the copilot-debug command",66"binary": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Binary executed with the command" }67}68*/69this.telemetry.sendMSFTTelemetryEvent('onboardDebug.commandExecuted', {70binary: args[0],71});7273for (const [key, prebaked] of Object.entries(testsStatuses)) {74if (args.includes(key)) {75return prebaked;76}77}7879let record = this.tryMatchExistingConfig(cwd, args);80if (!record || forceNew) {81this.interactor.isGenerating();82const result = await this.commandToConfig.convert(cwd, args, token);83if (!result.ok) {84return { kind: StartResultKind.NoConfig, text: result.text };85}8687record = {88args,89cwd,90folder: result.workspaceFolder,91inputs: [],92config: result.config!,93};9495/* __GDPR__96"onboardDebug.sessionConfigGenerated" : {97"owner": "connor4312",98"comment": "Reports a generated config for the copilot-debug command",99"binary": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Binary executed with the command" },100"debugType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Debug type generated" }101}102*/103this.telemetry.sendMSFTTelemetryEvent('onboardDebug.sessionConfigGenerated', {104binary: args[0],105debugType: record.config.configurations[0].type,106});107}108109const config = record.config.configurations[0];110const folder = record.folder && this.workspaceService.getWorkspaceFolder(URI.revive(record.folder));111if (!printOnly && record.config.tasks?.length) {112if (!(await this.interactor.ensureTask(folder, record.config.tasks[0]))) {113if (!save) { // if just saving, still let the user save even if they don't want the task114return { kind: StartResultKind.Cancelled };115}116}117}118119if (printOnly || save) {120this.saveConfigInLRU(record);121if (save) {122await this.save(record.config, folder);123}124return { kind: StartResultKind.Ok, folder, config };125}126127if (!this.hasMatchingExtension(config)) {128return { kind: StartResultKind.NeedExtension, debugType: config.type };129}130131const postInput = await this.launchConfigService.resolveConfigurationInputs(record.config, new Map(record.inputs), this.interactor);132if (!postInput) {133return { kind: StartResultKind.Cancelled };134}135136// inputs are saved to use as defaults in the next run137record.inputs = [...postInput.inputs];138this.saveConfigInLRU(record);139140return {141kind: StartResultKind.Ok,142folder,143config: postInput.config,144};145}146147private async save(launchConfig: { configurations: vscode.DebugConfiguration[]; inputs?: any[] }, folder: URI | undefined) {148await this.launchConfigService.add(folder, launchConfig);149if (folder) {150await this.launchConfigService.show(folder, launchConfig.configurations[0].name);151}152}153154private hasMatchingExtension(config: vscode.DebugConfiguration) {155for (const extension of this.extensionsService.allAcrossExtensionHosts) {156const debuggers = (extension.packageJSON as IPackageJson)?.contributes?.debuggers;157if (Array.isArray(debuggers) && debuggers.some(d => d && d.type === config.type)) {158return true;159}160}161162return false;163}164165private tryMatchExistingConfig(cwd: string, args: readonly string[]): IStoredData | undefined {166const stored = this.readStoredConfigs();167const exact = stored.findIndex(c => c.cwd === cwd && equals(c.args, args));168if (exact !== -1) {169return stored[exact];170}171172// could possibly do more advanced things here like reusing an existing config if only one arg was different173174return undefined;175}176177private readStoredConfigs(): readonly IStoredData[] {178return this.context.workspaceState.get<IStoredData[]>(STORAGE_KEY, []);179}180181private saveConfigInLRU(add: IStoredData) {182const configs = this.readStoredConfigs().slice();183const idx = configs.indexOf(add);184if (idx >= 1) {185configs.splice(idx, 1);186}187188configs.unshift(add);189while (configs.length > LRU_SIZE) {190configs.pop();191}192193this.context.workspaceState.update(STORAGE_KEY, configs);194}195}196197198