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