Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts
3296 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';3132registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender);33registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore);3435KeybindingsRegistry.registerCommandAndKeybindingRule({36id: CommentCommandId.NextThread,37handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => {38const activeEditor = getActiveEditor(accessor);39if (!activeEditor) {40return Promise.resolve();41}4243const controller = CommentController.get(activeEditor);44if (!controller) {45return Promise.resolve();46}47controller.nextCommentThread(true);48},49weight: KeybindingWeight.EditorContrib,50primary: KeyMod.Alt | KeyCode.F9,51});5253KeybindingsRegistry.registerCommandAndKeybindingRule({54id: CommentCommandId.PreviousThread,55handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => {56const activeEditor = getActiveEditor(accessor);57if (!activeEditor) {58return Promise.resolve();59}6061const controller = CommentController.get(activeEditor);62if (!controller) {63return Promise.resolve();64}65controller.previousCommentThread(true);66},67weight: KeybindingWeight.EditorContrib,68primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F969});7071registerAction2(class extends Action2 {72constructor() {73super({74id: CommentCommandId.NextCommentedRange,75title: {76value: nls.localize('comments.NextCommentedRange', "Go to Next Commented Range"),77original: 'Go to Next Commented Range'78},79category: {80value: nls.localize('commentsCategory', "Comments"),81original: 'Comments'82},83menu: [{84id: MenuId.CommandPalette,85when: CommentContextKeys.activeEditorHasCommentingRange86}],87keybinding: {88primary: KeyMod.Alt | KeyCode.F10,89weight: KeybindingWeight.EditorContrib,90when: CommentContextKeys.activeEditorHasCommentingRange91}92});93}94override run(accessor: ServicesAccessor, ...args: any[]): void {95const activeEditor = getActiveEditor(accessor);96if (!activeEditor) {97return;98}99100const controller = CommentController.get(activeEditor);101if (!controller) {102return;103}104controller.nextCommentThread(false);105}106});107108registerAction2(class extends Action2 {109constructor() {110super({111id: CommentCommandId.PreviousCommentedRange,112title: {113value: nls.localize('comments.previousCommentedRange', "Go to Previous Commented Range"),114original: 'Go to Previous Commented Range'115},116category: {117value: nls.localize('commentsCategory', "Comments"),118original: 'Comments'119},120menu: [{121id: MenuId.CommandPalette,122when: CommentContextKeys.activeEditorHasCommentingRange123}],124keybinding: {125primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F10,126weight: KeybindingWeight.EditorContrib,127when: CommentContextKeys.activeEditorHasCommentingRange128}129});130}131override run(accessor: ServicesAccessor, ...args: any[]): void {132const activeEditor = getActiveEditor(accessor);133if (!activeEditor) {134return;135}136137const controller = CommentController.get(activeEditor);138if (!controller) {139return;140}141controller.previousCommentThread(false);142}143});144145registerAction2(class extends Action2 {146constructor() {147super({148id: CommentCommandId.NextRange,149title: {150value: nls.localize('comments.nextCommentingRange', "Go to Next Commenting Range"),151original: 'Go to Next Commenting Range'152},153category: {154value: nls.localize('commentsCategory', "Comments"),155original: 'Comments'156},157menu: [{158id: MenuId.CommandPalette,159when: CommentContextKeys.activeEditorHasCommentingRange160}],161keybinding: {162primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.DownArrow),163weight: KeybindingWeight.EditorContrib,164when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(EditorContextKeys.focus, CommentContextKeys.commentFocused, ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Comments))))165}166});167}168169override run(accessor: ServicesAccessor, args?: { range: IRange; fileComment: boolean }): void {170const activeEditor = getActiveEditor(accessor);171if (!activeEditor) {172return;173}174175const controller = CommentController.get(activeEditor);176if (!controller) {177return;178}179controller.nextCommentingRange();180}181});182183registerAction2(class extends Action2 {184constructor() {185super({186id: CommentCommandId.PreviousRange,187title: {188value: nls.localize('comments.previousCommentingRange', "Go to Previous Commenting Range"),189original: 'Go to Previous Commenting Range'190},191category: {192value: nls.localize('commentsCategory', "Comments"),193original: 'Comments'194},195menu: [{196id: MenuId.CommandPalette,197when: CommentContextKeys.activeEditorHasCommentingRange198}],199keybinding: {200primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.UpArrow),201weight: KeybindingWeight.EditorContrib,202when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(EditorContextKeys.focus, CommentContextKeys.commentFocused, ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Comments))))203}204});205}206207override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {208const activeEditor = getActiveEditor(accessor);209if (!activeEditor) {210return;211}212213const controller = CommentController.get(activeEditor);214if (!controller) {215return;216}217controller.previousCommentingRange();218}219});220221registerAction2(class extends Action2 {222constructor() {223super({224id: CommentCommandId.ToggleCommenting,225title: {226value: nls.localize('comments.toggleCommenting', "Toggle Editor Commenting"),227original: 'Toggle Editor Commenting'228},229category: {230value: nls.localize('commentsCategory', "Comments"),231original: 'Comments'232},233menu: [{234id: MenuId.CommandPalette,235when: CommentContextKeys.WorkspaceHasCommenting236}]237});238}239override run(accessor: ServicesAccessor, ...args: any[]): void {240const commentService = accessor.get(ICommentService);241const enable = commentService.isCommentingEnabled;242commentService.enableCommenting(!enable);243}244});245246registerAction2(class extends Action2 {247constructor() {248super({249id: CommentCommandId.Add,250title: {251value: nls.localize('comments.addCommand', "Add Comment on Current Selection"),252original: 'Add Comment on Current Selection'253},254category: {255value: nls.localize('commentsCategory', "Comments"),256original: 'Comments'257},258menu: [{259id: MenuId.CommandPalette,260when: CommentContextKeys.activeCursorHasCommentingRange261}],262keybinding: {263primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC),264weight: KeybindingWeight.EditorContrib,265when: CommentContextKeys.activeCursorHasCommentingRange266}267});268}269270override async run(accessor: ServicesAccessor, args?: { range: IRange; fileComment: boolean }): Promise<void> {271const activeEditor = getActiveEditor(accessor);272if (!activeEditor) {273return;274}275276const controller = CommentController.get(activeEditor);277if (!controller) {278return;279}280281const position = args?.range ? new Range(args.range.startLineNumber, args.range.startLineNumber, args.range.endLineNumber, args.range.endColumn)282: (args?.fileComment ? undefined : activeEditor.getSelection());283await controller.addOrToggleCommentAtLine(position, undefined);284}285});286287registerAction2(class extends Action2 {288constructor() {289super({290id: CommentCommandId.FocusCommentOnCurrentLine,291title: {292value: nls.localize('comments.focusCommentOnCurrentLine', "Focus Comment on Current Line"),293original: 'Focus Comment on Current Line'294},295category: {296value: nls.localize('commentsCategory', "Comments"),297original: 'Comments'298},299f1: true,300precondition: CommentContextKeys.activeCursorHasComment,301});302}303override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {304const activeEditor = getActiveEditor(accessor);305if (!activeEditor) {306return;307}308309const controller = CommentController.get(activeEditor);310if (!controller) {311return;312}313const position = activeEditor.getSelection();314const notificationService = accessor.get(INotificationService);315let error = false;316try {317const commentAtLine = controller.getCommentsAtLine(position);318if (commentAtLine.length === 0) {319error = true;320} else {321await controller.revealCommentThread(commentAtLine[0].commentThread.threadId, undefined, false, CommentWidgetFocus.Widget);322}323} catch (e) {324error = true;325}326if (error) {327notificationService.error(nls.localize('comments.focusCommand.error', "The cursor must be on a line with a comment to focus the comment"));328}329}330});331332registerAction2(class extends Action2 {333constructor() {334super({335id: CommentCommandId.CollapseAll,336title: {337value: nls.localize('comments.collapseAll', "Collapse All Comments"),338original: 'Collapse All Comments'339},340category: {341value: nls.localize('commentsCategory', "Comments"),342original: 'Comments'343},344menu: [{345id: MenuId.CommandPalette,346when: CommentContextKeys.WorkspaceHasCommenting347}]348});349}350override run(accessor: ServicesAccessor, ...args: any[]): void {351getActiveController(accessor)?.collapseAll();352}353});354355registerAction2(class extends Action2 {356constructor() {357super({358id: CommentCommandId.ExpandAll,359title: {360value: nls.localize('comments.expandAll', "Expand All Comments"),361original: 'Expand All Comments'362},363category: {364value: nls.localize('commentsCategory', "Comments"),365original: 'Comments'366},367menu: [{368id: MenuId.CommandPalette,369when: CommentContextKeys.WorkspaceHasCommenting370}]371});372}373override run(accessor: ServicesAccessor, ...args: any[]): void {374getActiveController(accessor)?.expandAll();375}376});377378registerAction2(class extends Action2 {379constructor() {380super({381id: CommentCommandId.ExpandUnresolved,382title: {383value: nls.localize('comments.expandUnresolved', "Expand Unresolved Comments"),384original: 'Expand Unresolved Comments'385},386category: {387value: nls.localize('commentsCategory', "Comments"),388original: 'Comments'389},390menu: [{391id: MenuId.CommandPalette,392when: CommentContextKeys.WorkspaceHasCommenting393}]394});395}396override run(accessor: ServicesAccessor, ...args: any[]): void {397getActiveController(accessor)?.expandUnresolved();398}399});400401KeybindingsRegistry.registerCommandAndKeybindingRule({402id: CommentCommandId.Submit,403weight: KeybindingWeight.EditorContrib,404primary: KeyMod.CtrlCmd | KeyCode.Enter,405when: ctxCommentEditorFocused,406handler: (accessor, args) => {407const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();408if (activeCodeEditor instanceof SimpleCommentEditor) {409activeCodeEditor.getParentThread().submitComment();410}411}412});413414KeybindingsRegistry.registerCommandAndKeybindingRule({415id: CommentCommandId.Hide,416weight: KeybindingWeight.EditorContrib,417primary: KeyCode.Escape,418secondary: [KeyMod.Shift | KeyCode.Escape],419when: ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused),420handler: async (accessor, args) => {421const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();422const keybindingService = accessor.get(IKeybindingService);423// Unfortunate, but collapsing the comment thread might cause a dialog to show424// If we don't wait for the key up here, then the dialog will consume it and immediately close425await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide);426if (activeCodeEditor instanceof SimpleCommentEditor) {427activeCodeEditor.getParentThread().collapse();428} else if (activeCodeEditor) {429const controller = CommentController.get(activeCodeEditor);430if (!controller) {431return;432}433const notificationService = accessor.get(INotificationService);434const commentService = accessor.get(ICommentService);435let error = false;436try {437const activeComment = commentService.lastActiveCommentcontroller?.activeComment;438if (!activeComment) {439error = true;440} else {441controller.collapseAndFocusRange(activeComment.thread.threadId);442}443} catch (e) {444error = true;445}446if (error) {447notificationService.error(nls.localize('comments.focusCommand.error', "The cursor must be on a line with a comment to focus the comment"));448}449}450}451});452453export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null {454let activeTextEditorControl = accessor.get(IEditorService).activeTextEditorControl;455456if (isDiffEditor(activeTextEditorControl)) {457if (activeTextEditorControl.getOriginalEditor().hasTextFocus()) {458activeTextEditorControl = activeTextEditorControl.getOriginalEditor();459} else {460activeTextEditorControl = activeTextEditorControl.getModifiedEditor();461}462}463464if (!isCodeEditor(activeTextEditorControl) || !activeTextEditorControl.hasModel()) {465return null;466}467468return activeTextEditorControl;469}470471function getActiveController(accessor: ServicesAccessor): CommentController | undefined {472const activeEditor = getActiveEditor(accessor);473if (!activeEditor) {474return undefined;475}476477const controller = CommentController.get(activeEditor);478if (!controller) {479return undefined;480}481return controller;482}483484485486