Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts
13401 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 { Codicon } from '../../../../base/common/codicons.js';
7
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
8
import { autorun } from '../../../../base/common/observable.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import { localize } from '../../../../nls.js';
11
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
12
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
13
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
14
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
15
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
16
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
17
import { IsPhoneLayoutContext } from '../../../common/contextkeys.js';
18
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
19
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
20
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
21
import { ThemeIcon } from '../../../../base/common/themables.js';
22
import { IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js';
23
import { getSessionEditorComments } from '../../agentFeedback/browser/sessionEditorComments.js';
24
import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, MAX_CODE_REVIEWS_PER_SESSION_VERSION, PRReviewStateKind } from './codeReviewService.js';
25
import { CopilotCloudSessionType } from '../../../services/sessions/common/session.js';
26
27
registerSingleton(ICodeReviewService, CodeReviewService, InstantiationType.Delayed);
28
29
const canRunSessionCodeReviewContextKey = new RawContextKey<boolean>('sessions.canRunCodeReview', true, {
30
type: 'boolean',
31
description: localize('sessions.canRunCodeReview', "True when a new code review can be started for the active session version."),
32
});
33
34
function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disposable {
35
class RunSessionCodeReviewAction extends Action2 {
36
static readonly ID = 'sessions.codeReview.run';
37
38
constructor() {
39
super({
40
id: RunSessionCodeReviewAction.ID,
41
title: localize('sessions.runCodeReview', "Run Code Review"),
42
tooltip,
43
category: CHAT_CATEGORY,
44
icon,
45
precondition: ContextKeyExpr.and(
46
ChatContextKeys.hasAgentSessionChanges,
47
canRunSessionCodeReviewContextKey),
48
menu: [
49
{
50
id: MenuId.ChatEditingSessionChangesToolbar,
51
group: 'navigation',
52
order: 7,
53
when: ContextKeyExpr.and(
54
IsSessionsWindowContext,
55
ChatContextKeys.agentSessionType.notEqualsTo(CopilotCloudSessionType.id),
56
IsPhoneLayoutContext.negate(),
57
),
58
},
59
],
60
});
61
}
62
63
override async run(accessor: ServicesAccessor, sessionResource?: URI): Promise<void> {
64
const sessionManagementService = accessor.get(ISessionsManagementService);
65
const codeReviewService = accessor.get(ICodeReviewService);
66
const agentFeedbackService = accessor.get(IAgentFeedbackService);
67
68
const resource = URI.isUri(sessionResource)
69
? sessionResource
70
: sessionManagementService.activeSession.get()?.resource;
71
if (!resource) {
72
return;
73
}
74
75
// Get changes from ISession
76
const sessionData = sessionManagementService.getSession(resource);
77
const changes = sessionData?.changes.get();
78
if (!changes || changes.length === 0) {
79
return;
80
}
81
82
const files = getCodeReviewFilesFromSessionChanges(changes);
83
const version = getCodeReviewVersion(files);
84
85
// If there are existing comments (code review or PR review), navigate to the first one
86
const reviewState = codeReviewService.getReviewState(resource).get();
87
const prReviewState = codeReviewService.getPRReviewState(resource).get();
88
const reviewCount = reviewState.kind !== CodeReviewStateKind.Idle && reviewState.version === version ? reviewState.reviewCount : 0;
89
const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0;
90
const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0;
91
92
if (codeReviewCount > 0 || prReviewCount > 0) {
93
const comments = getSessionEditorComments(
94
resource,
95
agentFeedbackService.getFeedback(resource),
96
reviewState,
97
prReviewState,
98
);
99
const first = agentFeedbackService.getNextNavigableItem(resource, comments, true);
100
if (first) {
101
await agentFeedbackService.revealSessionComment(resource, first.id, first.resourceUri, first.range);
102
}
103
return;
104
}
105
106
if (reviewCount >= MAX_CODE_REVIEWS_PER_SESSION_VERSION) {
107
return;
108
}
109
110
111
codeReviewService.requestReview(resource, version, files);
112
}
113
}
114
115
return registerAction2(RunSessionCodeReviewAction) as Disposable;
116
}
117
118
class CodeReviewToolbarContribution extends Disposable implements IWorkbenchContribution {
119
120
static readonly ID = 'sessions.contrib.codeReviewToolbar';
121
122
private readonly _actionRegistration = this._register(new MutableDisposable<Disposable>());
123
124
constructor(
125
@IContextKeyService contextKeyService: IContextKeyService,
126
@ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService,
127
@ICodeReviewService private readonly _codeReviewService: ICodeReviewService,
128
) {
129
super();
130
131
const canRunCodeReviewContext = canRunSessionCodeReviewContextKey.bindTo(contextKeyService);
132
133
this._register(autorun(reader => {
134
const activeSession = this._sessionManagementService.activeSession.read(reader);
135
this._actionRegistration.clear();
136
137
const sessionResource = activeSession?.resource;
138
if (!sessionResource) {
139
canRunCodeReviewContext.set(false);
140
this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noSession', "No active session available for code review."), Codicon.codeReview);
141
return;
142
}
143
144
const changes = activeSession.changes.read(reader);
145
if (changes.length === 0) {
146
canRunCodeReviewContext.set(false);
147
this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noChanges', "No changes available for code review."), Codicon.codeReview);
148
return;
149
}
150
151
const files = getCodeReviewFilesFromSessionChanges(changes);
152
const version = getCodeReviewVersion(files);
153
const reviewState = this._codeReviewService.getReviewState(sessionResource).read(reader);
154
const prReviewState = this._codeReviewService.getPRReviewState(sessionResource).read(reader);
155
const reviewCount = reviewState.kind !== CodeReviewStateKind.Idle && reviewState.version === version ? reviewState.reviewCount : 0;
156
157
const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0;
158
const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0;
159
const totalCommentCount = codeReviewCount + prReviewCount;
160
161
let canRunCodeReview = true;
162
let tooltip = localize('sessions.runCodeReview.tooltip.default', "Run Code Review");
163
let icon = Codicon.codeReview;
164
165
if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === version) {
166
canRunCodeReview = false;
167
tooltip = localize('sessions.runCodeReview.tooltip.loading', "Creating code review...");
168
icon = Codicon.commentDraft;
169
} else if (totalCommentCount > 0) {
170
canRunCodeReview = true;
171
icon = Codicon.commentUnresolved;
172
tooltip = totalCommentCount === 1
173
? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.")
174
: localize('sessions.runCodeReview.tooltip.manyUnresolved', "{0} review comments unresolved.", totalCommentCount);
175
} else if (reviewCount >= MAX_CODE_REVIEWS_PER_SESSION_VERSION) {
176
canRunCodeReview = false;
177
tooltip = localize('sessions.runCodeReview.tooltip.limitReached', "Maximum of {0} code reviews reached for this session version.", MAX_CODE_REVIEWS_PER_SESSION_VERSION);
178
icon = Codicon.codeReview;
179
} else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version) {
180
canRunCodeReview = true;
181
tooltip = reviewState.didProduceComments
182
? localize('sessions.runCodeReview.tooltip.runAgain', "Run another code review.")
183
: localize('sessions.runCodeReview.tooltip.noCommentsRunAgain', "Previous code review produced no comments. Run code review again.");
184
icon = reviewState.didProduceComments ? Codicon.comment : Codicon.codeReview;
185
}
186
187
canRunCodeReviewContext.set(canRunCodeReview);
188
this._actionRegistration.value = registerSessionCodeReviewAction(tooltip, icon);
189
}));
190
}
191
}
192
193
registerWorkbenchContribution2(CodeReviewToolbarContribution.ID, CodeReviewToolbarContribution, WorkbenchPhase.AfterRestored);
194
195