Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts
5236 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 { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
7
import './media/review.css';
8
import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
9
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
10
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
11
import * as nls from '../../../../nls.js';
12
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
13
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
14
import { ICommentService } from './commentService.js';
15
import { ctxCommentEditorFocused, SimpleCommentEditor } from './simpleCommentEditor.js';
16
import { IEditorService } from '../../../services/editor/common/editorService.js';
17
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
18
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
19
import { CommentController, ID } from './commentsController.js';
20
import { IRange, Range } from '../../../../editor/common/core/range.js';
21
import { INotificationService } from '../../../../platform/notification/common/notification.js';
22
import { CommentContextKeys } from '../common/commentContextKeys.js';
23
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js';
24
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
25
import { accessibilityHelpIsShown, accessibleViewCurrentProviderId } from '../../accessibility/browser/accessibilityConfiguration.js';
26
import { CommentCommandId } from '../common/commentCommandIds.js';
27
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
28
import { CommentsInputContentProvider } from './commentsInputContentProvider.js';
29
import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js';
30
import { CommentWidgetFocus } from './commentThreadZoneWidget.js';
31
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
32
import { CommentThread, CommentThreadCollapsibleState, CommentThreadState } from '../../../../editor/common/languages.js';
33
34
registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender);
35
registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore);
36
37
KeybindingsRegistry.registerCommandAndKeybindingRule({
38
id: CommentCommandId.NextThread,
39
handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => {
40
const activeEditor = getActiveEditor(accessor);
41
if (!activeEditor) {
42
return Promise.resolve();
43
}
44
45
const controller = CommentController.get(activeEditor);
46
if (!controller) {
47
return Promise.resolve();
48
}
49
controller.nextCommentThread(true);
50
},
51
weight: KeybindingWeight.EditorContrib,
52
primary: KeyMod.Alt | KeyCode.F9,
53
});
54
55
KeybindingsRegistry.registerCommandAndKeybindingRule({
56
id: CommentCommandId.PreviousThread,
57
handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => {
58
const activeEditor = getActiveEditor(accessor);
59
if (!activeEditor) {
60
return Promise.resolve();
61
}
62
63
const controller = CommentController.get(activeEditor);
64
if (!controller) {
65
return Promise.resolve();
66
}
67
controller.previousCommentThread(true);
68
},
69
weight: KeybindingWeight.EditorContrib,
70
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F9
71
});
72
73
registerAction2(class extends Action2 {
74
constructor() {
75
super({
76
id: CommentCommandId.NextCommentedRange,
77
title: {
78
value: nls.localize('comments.NextCommentedRange', "Go to Next Commented Range"),
79
original: 'Go to Next Commented Range'
80
},
81
category: {
82
value: nls.localize('commentsCategory', "Comments"),
83
original: 'Comments'
84
},
85
menu: [{
86
id: MenuId.CommandPalette,
87
when: CommentContextKeys.activeEditorHasCommentingRange
88
}],
89
keybinding: {
90
primary: KeyMod.Alt | KeyCode.F10,
91
weight: KeybindingWeight.EditorContrib,
92
when: CommentContextKeys.activeEditorHasCommentingRange
93
}
94
});
95
}
96
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
97
const activeEditor = getActiveEditor(accessor);
98
if (!activeEditor) {
99
return;
100
}
101
102
const controller = CommentController.get(activeEditor);
103
if (!controller) {
104
return;
105
}
106
controller.nextCommentThread(false);
107
}
108
});
109
110
registerAction2(class extends Action2 {
111
constructor() {
112
super({
113
id: CommentCommandId.PreviousCommentedRange,
114
title: {
115
value: nls.localize('comments.previousCommentedRange', "Go to Previous Commented Range"),
116
original: 'Go to Previous Commented Range'
117
},
118
category: {
119
value: nls.localize('commentsCategory', "Comments"),
120
original: 'Comments'
121
},
122
menu: [{
123
id: MenuId.CommandPalette,
124
when: CommentContextKeys.activeEditorHasCommentingRange
125
}],
126
keybinding: {
127
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F10,
128
weight: KeybindingWeight.EditorContrib,
129
when: CommentContextKeys.activeEditorHasCommentingRange
130
}
131
});
132
}
133
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
134
const activeEditor = getActiveEditor(accessor);
135
if (!activeEditor) {
136
return;
137
}
138
139
const controller = CommentController.get(activeEditor);
140
if (!controller) {
141
return;
142
}
143
controller.previousCommentThread(false);
144
}
145
});
146
147
registerAction2(class extends Action2 {
148
constructor() {
149
super({
150
id: CommentCommandId.NextRange,
151
title: {
152
value: nls.localize('comments.nextCommentingRange', "Go to Next Commenting Range"),
153
original: 'Go to Next Commenting Range'
154
},
155
category: {
156
value: nls.localize('commentsCategory', "Comments"),
157
original: 'Comments'
158
},
159
menu: [{
160
id: MenuId.CommandPalette,
161
when: CommentContextKeys.activeEditorHasCommentingRange
162
}],
163
keybinding: {
164
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.DownArrow),
165
weight: KeybindingWeight.EditorContrib,
166
when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(EditorContextKeys.focus, CommentContextKeys.commentFocused, ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Comments))))
167
}
168
});
169
}
170
171
override run(accessor: ServicesAccessor, args?: { range: IRange; fileComment: boolean }): void {
172
const activeEditor = getActiveEditor(accessor);
173
if (!activeEditor) {
174
return;
175
}
176
177
const controller = CommentController.get(activeEditor);
178
if (!controller) {
179
return;
180
}
181
controller.nextCommentingRange();
182
}
183
});
184
185
registerAction2(class extends Action2 {
186
constructor() {
187
super({
188
id: CommentCommandId.PreviousRange,
189
title: {
190
value: nls.localize('comments.previousCommentingRange', "Go to Previous Commenting Range"),
191
original: 'Go to Previous Commenting Range'
192
},
193
category: {
194
value: nls.localize('commentsCategory', "Comments"),
195
original: 'Comments'
196
},
197
menu: [{
198
id: MenuId.CommandPalette,
199
when: CommentContextKeys.activeEditorHasCommentingRange
200
}],
201
keybinding: {
202
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.UpArrow),
203
weight: KeybindingWeight.EditorContrib,
204
when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(EditorContextKeys.focus, CommentContextKeys.commentFocused, ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Comments))))
205
}
206
});
207
}
208
209
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
210
const activeEditor = getActiveEditor(accessor);
211
if (!activeEditor) {
212
return;
213
}
214
215
const controller = CommentController.get(activeEditor);
216
if (!controller) {
217
return;
218
}
219
controller.previousCommentingRange();
220
}
221
});
222
223
registerAction2(class extends Action2 {
224
constructor() {
225
super({
226
id: CommentCommandId.ToggleCommenting,
227
title: {
228
value: nls.localize('comments.toggleCommenting', "Toggle Editor Commenting"),
229
original: 'Toggle Editor Commenting'
230
},
231
category: {
232
value: nls.localize('commentsCategory', "Comments"),
233
original: 'Comments'
234
},
235
menu: [{
236
id: MenuId.CommandPalette,
237
when: CommentContextKeys.WorkspaceHasCommenting
238
}]
239
});
240
}
241
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
242
const commentService = accessor.get(ICommentService);
243
const enable = commentService.isCommentingEnabled;
244
commentService.enableCommenting(!enable);
245
}
246
});
247
248
registerAction2(class extends Action2 {
249
constructor() {
250
super({
251
id: CommentCommandId.Add,
252
title: {
253
value: nls.localize('comments.addCommand', "Add Comment on Current Selection"),
254
original: 'Add Comment on Current Selection'
255
},
256
category: {
257
value: nls.localize('commentsCategory', "Comments"),
258
original: 'Comments'
259
},
260
menu: [{
261
id: MenuId.CommandPalette,
262
when: CommentContextKeys.activeCursorHasCommentingRange
263
}],
264
keybinding: {
265
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC),
266
weight: KeybindingWeight.EditorContrib,
267
when: CommentContextKeys.activeCursorHasCommentingRange
268
}
269
});
270
}
271
272
override async run(accessor: ServicesAccessor, args?: { range: IRange; fileComment: boolean }): Promise<void> {
273
const activeEditor = getActiveEditor(accessor);
274
if (!activeEditor) {
275
return;
276
}
277
278
const controller = CommentController.get(activeEditor);
279
if (!controller) {
280
return;
281
}
282
283
const position = args?.range ? new Range(args.range.startLineNumber, args.range.startLineNumber, args.range.endLineNumber, args.range.endColumn)
284
: (args?.fileComment ? undefined : activeEditor.getSelection());
285
await controller.addOrToggleCommentAtLine(position, undefined);
286
}
287
});
288
289
registerAction2(class extends Action2 {
290
constructor() {
291
super({
292
id: CommentCommandId.FocusCommentOnCurrentLine,
293
title: {
294
value: nls.localize('comments.focusCommentOnCurrentLine', "Focus Comment on Current Line"),
295
original: 'Focus Comment on Current Line'
296
},
297
category: {
298
value: nls.localize('commentsCategory', "Comments"),
299
original: 'Comments'
300
},
301
f1: true,
302
precondition: CommentContextKeys.activeCursorHasComment,
303
});
304
}
305
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
306
const activeEditor = getActiveEditor(accessor);
307
if (!activeEditor) {
308
return;
309
}
310
311
const controller = CommentController.get(activeEditor);
312
if (!controller) {
313
return;
314
}
315
const position = activeEditor.getSelection();
316
const notificationService = accessor.get(INotificationService);
317
let error = false;
318
try {
319
const commentAtLine = controller.getCommentsAtLine(position);
320
if (commentAtLine.length === 0) {
321
error = true;
322
} else {
323
await controller.revealCommentThread(commentAtLine[0].commentThread.threadId, undefined, false, CommentWidgetFocus.Widget);
324
}
325
} catch (e) {
326
error = true;
327
}
328
if (error) {
329
notificationService.error(nls.localize('comments.focusCommand.error', "The cursor must be on a line with a comment to focus the comment"));
330
}
331
}
332
});
333
334
function changeAllCollapseState(commentService: ICommentService, newState: (commentThread: CommentThread) => CommentThreadCollapsibleState) {
335
for (const resource of commentService.commentsModel.resourceCommentThreads) {
336
for (const thread of resource.commentThreads) {
337
thread.thread.collapsibleState = newState(thread.thread);
338
}
339
}
340
}
341
342
registerAction2(class extends Action2 {
343
constructor() {
344
super({
345
id: CommentCommandId.CollapseAll,
346
title: {
347
value: nls.localize('comments.collapseAll', "Collapse All Comments"),
348
original: 'Collapse All Comments'
349
},
350
category: {
351
value: nls.localize('commentsCategory', "Comments"),
352
original: 'Comments'
353
},
354
menu: [{
355
id: MenuId.CommandPalette,
356
when: CommentContextKeys.WorkspaceHasCommenting
357
}]
358
});
359
}
360
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
361
const commentService = accessor.get(ICommentService);
362
changeAllCollapseState(commentService, () => CommentThreadCollapsibleState.Collapsed);
363
}
364
});
365
366
registerAction2(class extends Action2 {
367
constructor() {
368
super({
369
id: CommentCommandId.ExpandAll,
370
title: {
371
value: nls.localize('comments.expandAll', "Expand All Comments"),
372
original: 'Expand All Comments'
373
},
374
category: {
375
value: nls.localize('commentsCategory', "Comments"),
376
original: 'Comments'
377
},
378
menu: [{
379
id: MenuId.CommandPalette,
380
when: CommentContextKeys.WorkspaceHasCommenting
381
}]
382
});
383
}
384
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
385
const commentService = accessor.get(ICommentService);
386
changeAllCollapseState(commentService, () => CommentThreadCollapsibleState.Expanded);
387
}
388
});
389
390
registerAction2(class extends Action2 {
391
constructor() {
392
super({
393
id: CommentCommandId.ExpandUnresolved,
394
title: {
395
value: nls.localize('comments.expandUnresolved', "Expand Unresolved Comments"),
396
original: 'Expand Unresolved Comments'
397
},
398
category: {
399
value: nls.localize('commentsCategory', "Comments"),
400
original: 'Comments'
401
},
402
menu: [{
403
id: MenuId.CommandPalette,
404
when: CommentContextKeys.WorkspaceHasCommenting
405
}]
406
});
407
}
408
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
409
const commentService = accessor.get(ICommentService);
410
changeAllCollapseState(commentService, (commentThread) => {
411
return commentThread.state === CommentThreadState.Unresolved ? CommentThreadCollapsibleState.Expanded : CommentThreadCollapsibleState.Collapsed;
412
});
413
}
414
});
415
416
KeybindingsRegistry.registerCommandAndKeybindingRule({
417
id: CommentCommandId.Submit,
418
weight: KeybindingWeight.EditorContrib,
419
primary: KeyMod.CtrlCmd | KeyCode.Enter,
420
when: ctxCommentEditorFocused,
421
handler: (accessor, args) => {
422
const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
423
if (activeCodeEditor instanceof SimpleCommentEditor) {
424
activeCodeEditor.getParentThread().submitComment();
425
}
426
}
427
});
428
429
KeybindingsRegistry.registerCommandAndKeybindingRule({
430
id: CommentCommandId.Hide,
431
weight: KeybindingWeight.EditorContrib,
432
primary: KeyCode.Escape,
433
secondary: [KeyMod.Shift | KeyCode.Escape],
434
when: ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused),
435
handler: async (accessor, args) => {
436
const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
437
const keybindingService = accessor.get(IKeybindingService);
438
const notificationService = accessor.get(INotificationService);
439
const commentService = accessor.get(ICommentService);
440
// Unfortunate, but collapsing the comment thread might cause a dialog to show
441
// If we don't wait for the key up here, then the dialog will consume it and immediately close
442
await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide);
443
if (activeCodeEditor instanceof SimpleCommentEditor) {
444
activeCodeEditor.getParentThread().collapse();
445
} else if (activeCodeEditor) {
446
const controller = CommentController.get(activeCodeEditor);
447
if (!controller) {
448
return;
449
}
450
451
let error = false;
452
try {
453
const activeComment = commentService.lastActiveCommentcontroller?.activeComment;
454
if (!activeComment) {
455
error = true;
456
} else {
457
controller.collapseAndFocusRange(activeComment.thread.threadId);
458
}
459
} catch (e) {
460
error = true;
461
}
462
if (error) {
463
notificationService.error(nls.localize('comments.focusCommand.error', "The cursor must be on a line with a comment to focus the comment"));
464
}
465
}
466
}
467
});
468
469
KeybindingsRegistry.registerCommandAndKeybindingRule({
470
id: CommentCommandId.Hide,
471
weight: KeybindingWeight.EditorContrib,
472
primary: KeyMod.CtrlCmd | KeyCode.Escape,
473
win: { primary: KeyMod.Alt | KeyCode.Backspace },
474
when: ContextKeyExpr.and(EditorContextKeys.focus, CommentContextKeys.commentWidgetVisible),
475
handler: async (accessor, args) => {
476
const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
477
const keybindingService = accessor.get(IKeybindingService);
478
// Unfortunate, but collapsing the comment thread might cause a dialog to show
479
// If we don't wait for the key up here, then the dialog will consume it and immediately close
480
await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide);
481
if (activeCodeEditor) {
482
const controller = CommentController.get(activeCodeEditor);
483
if (controller) {
484
await controller.collapseVisibleComments();
485
}
486
}
487
}
488
});
489
490
export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null {
491
let activeTextEditorControl = accessor.get(IEditorService).activeTextEditorControl;
492
493
if (isDiffEditor(activeTextEditorControl)) {
494
if (activeTextEditorControl.getOriginalEditor().hasTextFocus()) {
495
activeTextEditorControl = activeTextEditorControl.getOriginalEditor();
496
} else {
497
activeTextEditorControl = activeTextEditorControl.getModifiedEditor();
498
}
499
}
500
501
if (!isCodeEditor(activeTextEditorControl) || !activeTextEditorControl.hasModel()) {
502
return null;
503
}
504
505
return activeTextEditorControl;
506
}
507
508
509