Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineChat/vscode-node/inlineChatCommands.ts
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 * as vscode from 'vscode';
7
import { editorAgentName, getChatParticipantIdFromName } from '../../../platform/chat/common/chatAgents';
8
import { trimCommonLeadingWhitespace } from '../../../platform/chunking/node/naiveChunker';
9
import { IConfigurationService } from '../../../platform/configuration/common/configurationService';
10
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
11
import { isScenarioAutomation } from '../../../platform/env/common/envService';
12
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
13
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
14
import { ILogService } from '../../../platform/log/common/logService';
15
import { IParserService } from '../../../platform/parser/node/parserService';
16
import type { CodeReviewInput } from '../../../platform/review/common/reviewCommand';
17
import { IReviewService, ReviewComment, ReviewSuggestionChange } from '../../../platform/review/common/reviewService';
18
import { IScopeSelector } from '../../../platform/scopeSelection/common/scopeSelection';
19
import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';
20
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
21
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
22
import { createFencedCodeBlock } from '../../../util/common/markdown';
23
import { coalesce } from '../../../util/vs/base/common/arrays';
24
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
25
import { CancellationError, onBugIndicatingError } from '../../../util/vs/base/common/errors';
26
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
27
import * as path from '../../../util/vs/base/common/path';
28
import { URI } from '../../../util/vs/base/common/uri';
29
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
30
import { Intent } from '../../common/constants';
31
import { explainIntentPromptSnippet } from '../../intents/node/explainIntent';
32
import { ChatParticipantRequestHandler } from '../../prompt/node/chatParticipantRequestHandler';
33
import { sendReviewActionTelemetry } from '../../prompt/node/feedbackGenerator';
34
import { CurrentSelection } from '../../prompts/node/panel/currentSelection';
35
import { SymbolAtCursor } from '../../prompts/node/panel/symbolAtCursor';
36
import { reviewFileChanges, ReviewSession } from '../../review/node/doReview';
37
import { QuickFixesProvider, RefactorsProvider } from './inlineChatCodeActions';
38
import { NotebookExectionStatusBarItemProvider } from './inlineChatNotebookActions';
39
40
export function registerInlineChatCommands(accessor: ServicesAccessor): IDisposable {
41
const instaService = accessor.get(IInstantiationService);
42
const tabsAndEditorsService = accessor.get(ITabsAndEditorsService);
43
const scopeSelector = accessor.get(IScopeSelector);
44
const ignoreService = accessor.get(IIgnoreService);
45
const reviewService = accessor.get(IReviewService);
46
const logService = accessor.get(ILogService);
47
const telemetryService = accessor.get(ITelemetryService);
48
const extensionContext = accessor.get(IVSCodeExtensionContext);
49
const configurationService = accessor.get(IConfigurationService);
50
const parserService = accessor.get(IParserService);
51
52
const disposables = new DisposableStore();
53
const doExplain = async (arg0: any, fromPalette?: true) => {
54
let message = `/${Intent.Explain} `;
55
let selectedText;
56
let activeDocumentUri;
57
let explainingDiagnostics = false;
58
if (typeof arg0 === 'string' && arg0) {
59
message = arg0;
60
} else {
61
// First see whether we are explaining diagnostics
62
const emptySelection = CurrentSelection.getCurrentSelection(tabsAndEditorsService, true);
63
if (emptySelection) {
64
const severeDiagnostics = vscode.languages.getDiagnostics(emptySelection.activeDocument.uri);
65
const diagnosticsInSelection = severeDiagnostics.filter(d => !!d.range.intersection(emptySelection.range));
66
const filteredDiagnostics = QuickFixesProvider.getWarningOrErrorDiagnostics(diagnosticsInSelection);
67
if (filteredDiagnostics.length) {
68
message += QuickFixesProvider.getDiagnosticsAsText(severeDiagnostics);
69
explainingDiagnostics = true;
70
}
71
}
72
73
const selection = CurrentSelection.getCurrentSelection(tabsAndEditorsService);
74
if (!explainingDiagnostics && selection) {
75
message += explainIntentPromptSnippet;
76
selectedText = formatSelection({ languageId: selection.languageId, selectedText: selection.selectedText });
77
activeDocumentUri = selection.activeDocument.uri;
78
}
79
80
if (!explainingDiagnostics && emptySelection && fromPalette) {
81
// Scope selection may further refine the active selection if it was ambiguous
82
try {
83
const selectedScope = await SymbolAtCursor.getSelectedScope(
84
ignoreService,
85
configurationService,
86
tabsAndEditorsService,
87
scopeSelector,
88
parserService,
89
{ document: TextDocumentSnapshot.create(emptySelection.activeDocument), selection: emptySelection.range });
90
if (selectedScope && selectedScope.symbolAtCursorState && selectedScope.symbolAtCursorState.codeAtCursor) {
91
message += explainIntentPromptSnippet;
92
const languageId = selectedScope.symbolAtCursorState.document.languageId ?? '';
93
selectedText = formatSelection({ languageId, selectedText: selectedScope.symbolAtCursorState.codeAtCursor });
94
activeDocumentUri = emptySelection.activeDocument.uri;
95
}
96
} catch (ex) {
97
if (ex instanceof CancellationError) {
98
// If the user invoked Explain This from the palette and chooses not to select a scope, we should not submit the question to chat
99
return;
100
}
101
onBugIndicatingError(ex);
102
}
103
}
104
}
105
if (activeDocumentUri && selectedText && !await ignoreService.isCopilotIgnored(activeDocumentUri)) {
106
message += selectedText;
107
}
108
vscode.commands.executeCommand('workbench.action.chat.open', { query: message });
109
};
110
const doApplyReview = async (commentThread: vscode.CommentThread, revealNext = false) => {
111
const comment = reviewService.findReviewComment(commentThread);
112
if (!comment || !comment.suggestion) {
113
return;
114
}
115
const activeEditor = vscode.window.activeTextEditor;
116
if (!activeEditor || activeEditor.document.uri.toString() !== comment.document.uri.toString()) {
117
return;
118
}
119
const { edits } = await comment.suggestion;
120
activeEditor.edit(editBuilder => {
121
edits.forEach(edit => {
122
editBuilder.replace(edit.range, edit.newText);
123
});
124
});
125
126
if (revealNext) {
127
goToNextReview(commentThread, +1);
128
}
129
130
const totalComments = reviewService.getReviewComments().length;
131
reviewService.removeReviewComments([comment]);
132
sendReviewActionTelemetry(comment, totalComments, 'applySuggestion', logService, telemetryService, instaService);
133
};
134
const doContinueInInlineChat = async (commentThread: vscode.CommentThread) => {
135
const comment = reviewService.findReviewComment(commentThread);
136
if (!comment) {
137
return;
138
}
139
const totalComments = reviewService.getReviewComments().length;
140
const message = comment.body instanceof vscode.MarkdownString ? comment.body.value : comment.body;
141
reviewService.removeReviewComments([comment]);
142
await vscode.commands.executeCommand('vscode.editorChat.start', {
143
initialRange: commentThread.range,
144
message: `/fix ${message}`,
145
autoSend: true,
146
});
147
sendReviewActionTelemetry(comment, totalComments, 'continueInInlineChat', logService, telemetryService, instaService);
148
};
149
const doContinueInChat = async (thread: vscode.CommentThread) => {
150
const comment = reviewService.findReviewComment(thread);
151
if (!comment) {
152
return;
153
}
154
const totalComments = reviewService.getReviewComments().length;
155
const message = comment.body instanceof vscode.MarkdownString ? comment.body.value : comment.body;
156
await vscode.commands.executeCommand('workbench.action.chat.open', {
157
query: 'Explain your comment.',
158
isPartialQuery: true,
159
previousRequests: [
160
{
161
request: 'Review my code.',
162
response: `In file \`${path.basename(comment.uri.fsPath)}\` at line ${comment.range.start.line + 1}:
163
164
${message}`,
165
}
166
]
167
});
168
sendReviewActionTelemetry(comment, totalComments, 'continueInChat', logService, telemetryService, instaService);
169
};
170
const doDiscardReview = async (commentThread: vscode.CommentThread, revealNext = false) => {
171
if (revealNext) {
172
goToNextReview(commentThread, +1);
173
}
174
175
const reviewComment = reviewService.findReviewComment(commentThread);
176
if (reviewComment) {
177
const totalComments = reviewService.getReviewComments().length;
178
reviewService.removeReviewComments([reviewComment]);
179
sendReviewActionTelemetry(reviewComment, totalComments, 'discardComment', logService, telemetryService, instaService);
180
}
181
};
182
const doDiscardAllReview = async () => {
183
const comments = reviewService.getReviewComments();
184
if (comments.length) {
185
reviewService.removeReviewComments(comments);
186
sendReviewActionTelemetry(comments, comments.length, 'discardAllComments', logService, telemetryService, instaService);
187
}
188
};
189
const markReviewHelpful = async (comment: vscode.Comment) => {
190
const reviewComment = reviewService.findReviewComment(comment);
191
if (reviewComment) {
192
const commentThread = reviewService.findCommentThread(reviewComment);
193
if (commentThread) {
194
commentThread.contextValue = updateContextValue(commentThread.contextValue, 'markedAsHelpful', 'markedAsUnhelpful');
195
}
196
const totalComments = reviewService.getReviewComments().length;
197
sendReviewActionTelemetry(reviewComment, totalComments, 'helpful', logService, telemetryService, instaService);
198
}
199
};
200
const markReviewUnhelpful = async (comment: vscode.Comment) => {
201
const reviewComment = reviewService.findReviewComment(comment);
202
if (reviewComment) {
203
const commentThread = reviewService.findCommentThread(reviewComment);
204
if (commentThread) {
205
commentThread.contextValue = updateContextValue(commentThread.contextValue, 'markedAsUnhelpful', 'markedAsHelpful');
206
}
207
const totalComments = reviewService.getReviewComments().length;
208
sendReviewActionTelemetry(reviewComment, totalComments, 'unhelpful', logService, telemetryService, instaService);
209
}
210
};
211
const extensionMode = extensionContext.extensionMode;
212
if (typeof extensionMode === 'number' && (extensionMode !== vscode.ExtensionMode.Test || isScenarioAutomation)) {
213
reviewService.updateContextValues();
214
}
215
const goToNextReview = (currentThread: vscode.CommentThread | undefined, direction: number) => {
216
let newComment: ReviewComment | undefined;
217
if (currentThread) {
218
const reviewComment = reviewService.findReviewComment(currentThread);
219
if (!reviewComment) {
220
return;
221
}
222
const reviewComments = reviewService.getReviewComments();
223
const currentIndex = reviewComments.indexOf(reviewComment);
224
const newIndex = (currentIndex + direction + reviewComments.length) % reviewComments.length;
225
newComment = reviewComments[newIndex];
226
} else {
227
const reviewComments = reviewService.getReviewComments();
228
newComment = reviewComments[direction > 0 ? 0 : reviewComments.length - 1];
229
}
230
const newThread = newComment && reviewService.findCommentThread(newComment);
231
if (!newThread) {
232
return;
233
}
234
if (direction !== 0) {
235
(newThread as unknown as vscode.CommentThread2).reveal();
236
}
237
instaService.invokeFunction(fetchSuggestion, newThread);
238
};
239
const doGenerate = () => {
240
return vscode.commands.executeCommand('vscode.editorChat.start', { message: '/generate ' });
241
};
242
const doFix = () => {
243
const activeDocument = vscode.window.activeTextEditor;
244
if (!activeDocument) {
245
return;
246
}
247
const activeSelection = activeDocument.selection;
248
const diagnostics = vscode.languages.getDiagnostics(activeDocument.document.uri).filter(diagnostic => {
249
return !!activeSelection.intersection(diagnostic.range);
250
}).map(d => d.message).join(', ');
251
return vscode.commands.executeCommand('vscode.editorChat.start', { message: `/${Intent.Fix} ${diagnostics}`, autoSend: true, initialRange: vscode.window.activeTextEditor?.selection });
252
};
253
254
const doGenerateAltText = async (arg: unknown) => {
255
if (arg && typeof arg === 'object' && 'isUrl' in arg && 'resolvedImagePath' in arg && typeof arg.resolvedImagePath === 'string' && 'type' in arg) {
256
const baseQuery = 'Create an alt text description that is helpful for screen readers and people who are blind or have visual impairment. Never start alt text with "Image of..." or "Picture of...". Please clearly identify the primary subject or subjects of the image. Describe what the subject is doing, if applicable. Please add a short description of the wider environment. If there is text in the image please transcribe and include it. Please describe the emotional tone of the image, if applicable. Do not use single or double quotes in the alt text.';
257
const fullQuery = arg.type === 'generate' ? baseQuery : `Refine the existing alt text for clarity and usefulness for screen readers. ${baseQuery}`;
258
259
const uri = arg.isUrl ? URI.parse(arg.resolvedImagePath) : URI.file(arg.resolvedImagePath);
260
return vscode.commands.executeCommand('vscode.editorChat.start', { message: fullQuery, attachments: [uri], autoSend: true, initialRange: vscode.window.activeTextEditor?.selection });
261
}
262
};
263
264
// register commands
265
disposables.add(vscode.commands.registerCommand('github.copilot.chat.explain', doExplain));
266
disposables.add(vscode.commands.registerCommand('github.copilot.chat.explain.palette', () => doExplain(undefined, true)));
267
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review', () => instaService.createInstance(ReviewSession).review('selection', vscode.ProgressLocation.Notification)));
268
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.stagedChanges', () => instaService.createInstance(ReviewSession).review('index', vscode.ProgressLocation.Notification)));
269
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.unstagedChanges', () => instaService.createInstance(ReviewSession).review('workingTree', vscode.ProgressLocation.Notification)));
270
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.changes', () => instaService.createInstance(ReviewSession).review('all', vscode.ProgressLocation.Notification)));
271
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.stagedFileChange', (resource: vscode.SourceControlResourceState) => {
272
return instaService.createInstance(ReviewSession).review({ group: 'index', file: resource.resourceUri }, vscode.ProgressLocation.Notification);
273
}));
274
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.unstagedFileChange', (resource: vscode.SourceControlResourceState) => {
275
return instaService.createInstance(ReviewSession).review({ group: 'workingTree', file: resource.resourceUri }, vscode.ProgressLocation.Notification);
276
}));
277
disposables.add(vscode.commands.registerCommand('github.copilot.chat.codeReview.run', (input: CodeReviewInput) => {
278
return instaService.invokeFunction(reviewFileChanges, input);
279
}));
280
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.apply', doApplyReview));
281
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.applyAndNext', (commentThread: vscode.CommentThread) => doApplyReview(commentThread, true)));
282
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.applyShort', (commentThread: vscode.CommentThread) => doApplyReview(commentThread, true)));
283
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.continueInInlineChat', doContinueInInlineChat));
284
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.continueInChat', doContinueInChat));
285
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.discard', doDiscardReview));
286
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.discardAndNext', (commentThread: vscode.CommentThread) => doDiscardReview(commentThread, true)));
287
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.discardShort', (commentThread: vscode.CommentThread) => doDiscardReview(commentThread, true)));
288
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.discardAll', doDiscardAllReview));
289
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.markHelpful', markReviewHelpful));
290
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.markUnhelpful', markReviewUnhelpful));
291
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.previous', thread => goToNextReview(thread, -1)));
292
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.next', thread => goToNextReview(thread, +1)));
293
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.current', thread => goToNextReview(thread, 0)));
294
disposables.add(vscode.commands.registerCommand('github.copilot.chat.generate', doGenerate));
295
disposables.add(vscode.commands.registerCommand('github.copilot.chat.fix', doFix));
296
disposables.add(vscode.commands.registerCommand('github.copilot.chat.generateAltText', doGenerateAltText));
297
// register code actions
298
disposables.add(vscode.languages.registerCodeActionsProvider('*', instaService.createInstance(QuickFixesProvider), {
299
providedCodeActionKinds: QuickFixesProvider.providedCodeActionKinds,
300
}));
301
disposables.add(vscode.languages.registerCodeActionsProvider('*', instaService.createInstance(RefactorsProvider), {
302
providedCodeActionKinds: RefactorsProvider.providedCodeActionKinds,
303
}));
304
disposables.add(vscode.notebooks.registerNotebookCellStatusBarItemProvider(
305
'jupyter-notebook',
306
instaService.createInstance(NotebookExectionStatusBarItemProvider)
307
));
308
309
return disposables;
310
}
311
312
function fetchSuggestion(accessor: ServicesAccessor, thread: vscode.CommentThread) {
313
const logService = accessor.get(ILogService);
314
const reviewService = accessor.get(IReviewService);
315
const instantiationService = accessor.get(IInstantiationService);
316
const comment = reviewService.findReviewComment(thread);
317
if (!comment || comment.suggestion || comment.skipSuggestion) {
318
if (comment?.suggestion && 'edits' in comment.suggestion && comment.suggestion.edits.length && thread.contextValue?.includes('hasNoSuggestion')) {
319
thread.contextValue = updateContextValue(thread.contextValue, 'hasSuggestion', 'hasNoSuggestion');
320
}
321
return;
322
}
323
comment.suggestion = (async () => {
324
const message = comment.body instanceof vscode.MarkdownString ? comment.body.value : comment.body;
325
const document = comment.document;
326
327
const selection = new vscode.Selection(comment.range.start, comment.range.end);
328
const textEditor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === document.uri.toString()) ??
329
vscode.window.activeTextEditor ??
330
await vscode.window.showTextDocument(document.document, { preserveFocus: true, preview: false });
331
332
const command = Intent.Fix;
333
const prompt = message;
334
const request: vscode.ChatRequest = {
335
location: vscode.ChatLocation.Editor,
336
location2: new vscode.ChatRequestEditorData(textEditor, document.document, selection, selection),
337
command,
338
prompt,
339
references: [],
340
attempt: 0,
341
enableCommandDetection: false,
342
isParticipantDetected: false,
343
toolReferences: [],
344
toolInvocationToken: undefined as never,
345
model: null!,
346
tools: new Map(),
347
id: '1',
348
sessionId: '1',
349
sessionResource: vscode.Uri.parse('chat:/1'),
350
hasHooksEnabled: false,
351
};
352
let markdown = '';
353
const edits: ReviewSuggestionChange[] = [];
354
const stream = new ChatResponseStreamImpl((value) => {
355
if (value instanceof vscode.ChatResponseTextEditPart && value.edits.length > 0) {
356
edits.push(...value.edits.map(e => ({
357
range: e.range,
358
newText: e.newText,
359
oldText: document.getText(e.range),
360
})).filter(e => e.newText !== e.oldText));
361
} else if (value instanceof vscode.ChatResponseMarkdownPart) {
362
markdown += value.value.value;
363
}
364
}, () => { }, undefined, undefined, undefined, () => Promise.resolve(undefined));
365
366
const requestHandler = instantiationService.createInstance(ChatParticipantRequestHandler, [], request, stream, CancellationToken.None, {
367
agentId: getChatParticipantIdFromName(editorAgentName),
368
agentName: editorAgentName,
369
intentId: request.command,
370
}, () => false, undefined);
371
const result = await requestHandler.getResult();
372
if (result.errorDetails) {
373
throw new Error(result.errorDetails.message);
374
}
375
const suggestion = { markdown, edits };
376
comment.suggestion = suggestion;
377
reviewService.updateReviewComment(comment);
378
thread.contextValue = edits.length
379
? updateContextValue(thread.contextValue, 'hasSuggestion', 'hasNoSuggestion')
380
: updateContextValue(thread.contextValue, 'hasNoSuggestion', 'hasSuggestion');
381
return suggestion;
382
})()
383
.catch(err => {
384
logService.error(err, 'Error fetching suggestion');
385
comment.suggestion = {
386
markdown: `Error fetching suggestion: ${err?.message}`,
387
edits: [],
388
};
389
reviewService.updateReviewComment(comment);
390
return comment.suggestion;
391
});
392
reviewService.updateReviewComment(comment);
393
}
394
395
function updateContextValue(value: string | undefined, add: string, remove: string) {
396
return (value ? value.split(',') : [])
397
.filter(v => v !== add && v !== remove)
398
.concat(add)
399
.sort()
400
.join(',');
401
}
402
403
function formatSelection(selection: {
404
languageId: string;
405
selectedText: string;
406
fileName?: string;
407
}): string {
408
const fileContext = selection.fileName ? `From the file: ${path.basename(selection.fileName)}\n` : '';
409
const { trimmedLines } = trimCommonLeadingWhitespace(selection.selectedText.split(/\r?\n/g));
410
return `\n\n${fileContext}${createFencedCodeBlock(selection.languageId, coalesce(trimmedLines).join('\n'))}\n\n`;
411
}
412
413