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