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