From f662f12a406d16643873228d03841e80e510bfa0 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 18 Jul 2025 12:08:12 +0200 Subject: [PATCH] Review individual file changes --- package.json | 34 +++++++++++++++++++ package.nls.json | 1 + .../vscode-node/inlineChatCommands.ts | 8 +++++ src/extension/review/node/doReview.ts | 6 ++-- .../review/node/githubReviewAgent.ts | 26 ++++++++++++-- 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b0a6893d1..b0fd5f4af 100644 --- a/package.json +++ b/package.json @@ -1898,6 +1898,20 @@ "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "GitHub Copilot" }, + { + "command": "github.copilot.chat.review.stagedFileChange", + "title": "%github.copilot.command.reviewFileChange%", + "icon": "$(code-review)", + "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", + "category": "GitHub Copilot" + }, + { + "command": "github.copilot.chat.review.unstagedFileChange", + "title": "%github.copilot.command.reviewFileChange%", + "icon": "$(code-review)", + "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", + "category": "GitHub Copilot" + }, { "command": "github.copilot.chat.review.changes.cancel", "title": "%github.copilot.command.reviewChanges.cancel%", @@ -3067,6 +3081,14 @@ "command": "github.copilot.chat.review.changes", "when": "false" }, + { + "command": "github.copilot.chat.review.stagedFileChange", + "when": "false" + }, + { + "command": "github.copilot.chat.review.unstagedFileChange", + "when": "false" + }, { "command": "github.copilot.chat.review.changes.cancel", "when": "false" @@ -3334,6 +3356,18 @@ "group": "inline@-3" } ], + "scm/resourceState/context": [ + { + "command": "github.copilot.chat.review.stagedFileChange", + "group": "3_copilot", + "when": "github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == index" + }, + { + "command": "github.copilot.chat.review.unstagedFileChange", + "group": "3_copilot", + "when": "github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == workingTree" + } + ], "scm/inputBox": [ { "command": "github.copilot.git.generateCommitMessage", diff --git a/package.nls.json b/package.nls.json index 5f8847931..d4a6ad18f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,6 +15,7 @@ "github.copilot.command.reviewUnstagedChanges": "Code Review - Unstaged Changes", "github.copilot.command.reviewChanges": "Code Review - Uncommitted Changes", "github.copilot.command.reviewChanges.cancel": "Code Review - Cancel", + "github.copilot.command.reviewFileChange": "Review Changes", "github.copilot.command.gotoPreviousReviewSuggestion": "Previous Suggestion", "github.copilot.command.gotoNextReviewSuggestion": "Next Suggestion", "github.copilot.command.continueReviewInInlineChat": "Discard and Copy to Inline Chat", diff --git a/src/extension/inlineChat/vscode-node/inlineChatCommands.ts b/src/extension/inlineChat/vscode-node/inlineChatCommands.ts index 3380b5d51..786b939da 100644 --- a/src/extension/inlineChat/vscode-node/inlineChatCommands.ts +++ b/src/extension/inlineChat/vscode-node/inlineChatCommands.ts @@ -314,6 +314,14 @@ ${message}`, disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.stagedChanges', () => doReview(...instaService.invokeFunction(getServicesForReview), 'index', vscode.ProgressLocation.SourceControl))); disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.unstagedChanges', () => doReview(...instaService.invokeFunction(getServicesForReview), 'workingTree', vscode.ProgressLocation.SourceControl))); disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.changes', () => doReview(...instaService.invokeFunction(getServicesForReview), 'all', vscode.ProgressLocation.SourceControl))); + disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.stagedFileChange', (resource: vscode.SourceControlResourceState) => { + vscode.window.showTextDocument(resource.resourceUri, { preview: false }); + return doReview(...instaService.invokeFunction(getServicesForReview), { group: 'index', file: resource.resourceUri }, vscode.ProgressLocation.SourceControl); + })); + disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.unstagedFileChange', (resource: vscode.SourceControlResourceState) => { + vscode.window.showTextDocument(resource.resourceUri, { preview: false }); + return doReview(...instaService.invokeFunction(getServicesForReview), { group: 'workingTree', file: resource.resourceUri }, vscode.ProgressLocation.SourceControl); + })); disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.changes.cancel', () => cancelReview(vscode.ProgressLocation.SourceControl, instaService.invokeFunction(accessor => accessor.get(IRunCommandExecutionService))))); disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.apply', doApplyReview)); disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.applyAndNext', (commentThread: vscode.CommentThread) => doApplyReview(commentThread, true))); diff --git a/src/extension/review/node/doReview.ts b/src/extension/review/node/doReview.ts index 1f4c958c1..531ab393c 100644 --- a/src/extension/review/node/doReview.ts +++ b/src/extension/review/node/doReview.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { TextEditor } from 'vscode'; +import type { TextEditor, Uri } from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService'; import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot'; @@ -69,7 +69,7 @@ export async function doReview( workspaceService: IWorkspaceService, commandService: IRunCommandExecutionService, notificationService: INotificationService, - group: 'selection' | 'index' | 'workingTree' | 'all' | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] }, + group: 'selection' | 'index' | 'workingTree' | 'all' | { group: 'index' | 'workingTree'; file: Uri } | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] }, progressLocation: ProgressLocation, cancellationToken?: CancellationToken ): Promise { @@ -121,7 +121,7 @@ export async function doReview( try { const copilotToken = await authService.getCopilotToken(); const canUseGitHubAgent = (group === 'index' || group === 'workingTree' || group === 'all' || typeof group === 'object') && copilotToken.isCopilotCodeReviewEnabled; - result = canUseGitHubAgent ? await githubReview(logService, gitExtensionService, authService, capiClientService, domainService, fetcherService, envService, ignoreService, workspaceService, group, progress, tokenSource.token) : await review(instantiationService, gitExtensionService, workspaceService, group, editor, progress, tokenSource.token); + result = canUseGitHubAgent ? await githubReview(logService, gitExtensionService, authService, capiClientService, domainService, fetcherService, envService, ignoreService, workspaceService, group, progress, tokenSource.token) : await review(instantiationService, gitExtensionService, workspaceService, typeof group === 'object' && 'group' in group ? group.group : group, editor, progress, tokenSource.token); } catch (err) { result = { type: 'error', reason: err.message, severity: err.severity }; } finally { diff --git a/src/extension/review/node/githubReviewAgent.ts b/src/extension/review/node/githubReviewAgent.ts index 1ae8e53ad..52da27f1e 100644 --- a/src/extension/review/node/githubReviewAgent.ts +++ b/src/extension/review/node/githubReviewAgent.ts @@ -39,7 +39,7 @@ export async function githubReview( envService: IEnvService, ignoreService: IIgnoreService, workspaceService: IWorkspaceService, - group: 'index' | 'workingTree' | 'all' | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] }, + group: 'index' | 'workingTree' | 'all' | { group: 'index' | 'workingTree'; file: Uri } | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] }, progress: Progress, cancellationToken: CancellationToken ): Promise { @@ -76,7 +76,7 @@ export async function githubReview( })); return changes; }))).flat() - : await Promise.all(group.patches.map(async patch => { + : 'repositoryRoot' in group ? await Promise.all(group.patches.map(async patch => { const uri = Uri.parse(patch.fileUri); const document = await workspaceService.openTextDocument(uri).then(undefined, () => undefined); if (!document) { @@ -92,7 +92,27 @@ export async function githubReview( after, document, }; - }))).filter((change): change is NonNullable => !!change); + })) + : await (async () => { + const { group: g, file } = group; + const repository = git.getRepository(file); + const document = await workspaceService.openTextDocument(file).then(undefined, () => undefined); + if (!repository || !document) { + return []; + } + const before = await (g === 'index' ? repository.show('HEAD', file.fsPath).catch(() => '') : repository.show('', file.fsPath).catch(() => '')); + const after = g === 'index' ? await (repository.show('', file.fsPath).catch(() => '')) : document.getText(); + const relativePath = path.relative(repository.rootUri.fsPath, file.fsPath); + return [ + { + repository, + relativePath: process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath, + before, + after, + document, + } + ]; + })()).filter((change): change is NonNullable => !!change); if (!changes.length) { return { type: 'success', comments: [] };