diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index baf38d7c6fb..2ba38dfdf9e 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -5,6 +5,7 @@ "publisher": "vscode", "license": "MIT", "enabledApiProposals": [ + "activeComment", "authSession", "chatAgents2", "defaultChatAgent", diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 98a479f4b9a..538c754e1b3 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -242,7 +242,7 @@ export class MainThreadCommentController implements ICommentController { } private readonly _threads: Map> = new Map>(); - public activeCommentThread?: MainThreadCommentThread; + public activeEditingCommentThread?: MainThreadCommentThread; get features(): CommentProviderFeatures { return this._features; @@ -258,6 +258,10 @@ export class MainThreadCommentController implements ICommentController { private _features: CommentProviderFeatures ) { } + async setActiveCommentAndThread(commentInfo: { thread: languages.CommentThread; comment?: languages.Comment } | undefined) { + return this._proxy.$setActiveComment(this._handle, commentInfo ? { commentThreadHandle: commentInfo.thread.commentThreadHandle, uniqueIdInThread: commentInfo.comment?.uniqueIdInThread } : undefined); + } + updateFeatures(features: CommentProviderFeatures) { this._features = features; } @@ -357,7 +361,7 @@ export class MainThreadCommentController implements ICommentController { } updateInput(input: string) { - const thread = this.activeCommentThread; + const thread = this.activeEditingCommentThread; if (thread && thread.input) { const commentInput = thread.input; @@ -477,8 +481,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _handlers = new Map(); private _commentControllers = new Map(); - private _activeCommentThread?: MainThreadCommentThread; - private readonly _activeCommentThreadDisposables = this._register(new DisposableStore()); + private _activeEditingCommentThread?: MainThreadCommentThread; + private readonly _activeEditingCommentThreadDisposables = this._register(new DisposableStore()); private _openViewListener: IDisposable | null = null; @@ -493,7 +497,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); this._commentService.unregisterCommentController(); - this._register(this._commentService.onDidChangeActiveCommentThread(async thread => { + this._register(this._commentService.onDidChangeActiveEditingCommentThread(async thread => { const handle = (thread as MainThreadCommentThread).controllerHandle; const controller = this._commentControllers.get(handle); @@ -501,9 +505,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments return; } - this._activeCommentThreadDisposables.clear(); - this._activeCommentThread = thread as MainThreadCommentThread; - controller.activeCommentThread = this._activeCommentThread; + this._activeEditingCommentThreadDisposables.clear(); + this._activeEditingCommentThread = thread as MainThreadCommentThread; + controller.activeEditingCommentThread = this._activeEditingCommentThread; })); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d24c57433a8..78eda6ea257 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2403,6 +2403,7 @@ export interface ExtHostCommentsShape { $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): void; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined>; $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: languages.Comment, reaction: languages.CommentReaction): Promise; + $setActiveComment(controllerHandle: number, commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number } | undefined): Promise; } export interface INotebookSelectionChangeEvent { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 91e14ee9a07..95db9c67fee 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -168,6 +168,16 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo commentController.$createCommentThreadTemplate(uriComponents, range); } + async $setActiveComment(controllerHandle: number, commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number }): Promise { + const commentController = this._commentControllers.get(controllerHandle); + + if (!commentController) { + return; + } + + commentController.$setActiveComment(commentInfo ?? undefined); + } + async $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: IRange) { const commentController = this._commentControllers.get(commentControllerHandle); @@ -582,6 +592,19 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo proxy.$updateCommentControllerFeatures(this.handle, { options: this._options }); } + private _activeComment: vscode.Comment | undefined; + + get activeComment(): vscode.Comment | undefined { + checkProposedApiEnabled(this._extension, 'activeComment'); + return this._activeComment; + } + + private _activeThread: vscode.CommentThread2 | undefined; + + get activeThread(): vscode.CommentThread2 | undefined { + checkProposedApiEnabled(this._extension, 'activeComment'); + return this._activeThread; + } private _localDisposables: types.Disposable[]; readonly value: vscode.CommentController; @@ -604,6 +627,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set commentingRangeProvider(commentingRangeProvider: vscode.CommentingRangeProvider | undefined) { that.commentingRangeProvider = commentingRangeProvider; }, get reactionHandler(): ReactionHandler | undefined { return that.reactionHandler; }, set reactionHandler(handler: ReactionHandler | undefined) { that.reactionHandler = handler; }, + // get activeComment(): vscode.Comment | undefined { return that.activeComment; }, + get activeThread(): vscode.CommentThread2 | undefined { return that.activeThread; }, createCommentThread(uri: vscode.Uri, range: vscode.Range | undefined, comments: vscode.Comment[]): vscode.CommentThread | vscode.CommentThread2 { return that.createCommentThread(uri, range, comments).value; }, @@ -627,6 +652,19 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentThread; } + $setActiveComment(commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number } | undefined) { + if (!commentInfo) { + this._activeComment = undefined; + this._activeThread = undefined; + return; + } + const thread = this._threads.get(commentInfo.commentThreadHandle); + if (thread) { + this._activeComment = commentInfo.uniqueIdInThread ? thread.getCommentByUniqueId(commentInfo.uniqueIdInThread) : undefined; + this._activeThread = thread; + } + } + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined): ExtHostCommentThread { const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true); commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 7197063ec80..13ddcb48f1e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -166,6 +166,17 @@ export class CommentNode extends Disposable { this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { this.toggleToolbarHidden(true); })); + + this.activeCommentListeners(); + } + + private activeCommentListeners() { + this._register(dom.addDisposableListener(this._domNode, dom.EventType.FOCUS_IN, () => { + this.commentService.setActiveCommentAndThread(this.owner, { thread: this.commentThread, comment: this.comment }); + }, true)); + this._register(dom.addDisposableListener(this._domNode, dom.EventType.FOCUS_OUT, () => { + this.commentService.setActiveCommentAndThread(this.owner, undefined); + }, true)); } private createScroll(container: HTMLElement, body: HTMLElement) { @@ -502,14 +513,16 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: this.commentBodyValue }; - this.commentService.setActiveCommentThread(commentThread); + this.commentService.setActiveEditingCommentThread(commentThread); + this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment }); this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => { commentThread.input = { uri: this._commentEditor!.getModel()!.uri, value: this.commentBodyValue }; - this.commentService.setActiveCommentThread(commentThread); + this.commentService.setActiveEditingCommentThread(commentThread); + this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment }); })); this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { @@ -519,7 +532,8 @@ export class CommentNode extends Disposable { const input = commentThread.input; input.value = newVal; commentThread.input = input; - this.commentService.setActiveCommentThread(commentThread); + this.commentService.setActiveEditingCommentThread(commentThread); + this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment }); } } })); diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index d2c54a4c9f9..e4d45918616 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -233,11 +233,20 @@ export class CommentReply extends Disposable { private createTextModelListener(commentEditor: ICodeEditor, commentForm: HTMLElement) { this._commentThreadDisposables.push(commentEditor.onDidFocusEditorWidget(() => { - this._commentThread.input = { - uri: commentEditor.getModel()!.uri, - value: commentEditor.getValue() - }; - this.commentService.setActiveCommentThread(this._commentThread); + // Add a setTimeout so that the blur event doesn't fire before the focus event + // https://github.com/microsoft/vscode/blob/f6d945edbdc1b2e8a176624fdf612bb61468944f/src/vs/base/browser/dom.ts#L1322-L1328 + setTimeout(() => { + this._commentThread.input = { + uri: commentEditor.getModel()!.uri, + value: commentEditor.getValue() + }; + this.commentService.setActiveEditingCommentThread(this._commentThread); + this.commentService.setActiveCommentAndThread(this.owner, { thread: this._commentThread }); + }, 0); + })); + + this._commentThreadDisposables.push(commentEditor.onDidBlurEditorWidget(() => { + this.commentService.setActiveCommentAndThread(this.owner, undefined); })); this._commentThreadDisposables.push(commentEditor.getModel()!.onDidChangeContent(() => { @@ -247,7 +256,7 @@ export class CommentReply extends Disposable { newInput.value = modelContent; this._commentThread.input = newInput; } - this.commentService.setActiveCommentThread(this._commentThread); + this.commentService.setActiveEditingCommentThread(this._commentThread); })); this._commentThreadDisposables.push(this._commentThread.onDidChangeInput(input => { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index de319272c0e..2f72753c28c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -67,6 +67,7 @@ export interface ICommentController { toggleReaction(uri: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; getDocumentComments(resource: URI, token: CancellationToken): Promise; getNotebookComments(resource: URI, token: CancellationToken): Promise; + setActiveCommentAndThread(commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; } export interface IContinueOnCommentProvider { @@ -79,7 +80,7 @@ export interface ICommentService { readonly onDidSetAllCommentThreads: Event; readonly onDidUpdateCommentThreads: Event; readonly onDidUpdateNotebookCommentThreads: Event; - readonly onDidChangeActiveCommentThread: Event; + readonly onDidChangeActiveEditingCommentThread: Event; readonly onDidChangeCurrentCommentThread: Event; readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; @@ -105,8 +106,9 @@ export interface ICommentService { updateCommentingRanges(ownerId: string): void; hasReactionHandler(owner: string): boolean; toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; - setActiveCommentThread(commentThread: CommentThread | null): void; + setActiveEditingCommentThread(commentThread: CommentThread | null): void; setCurrentCommentThread(commentThread: CommentThread | undefined): void; + setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; enableCommenting(enable: boolean): void; registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; @@ -138,8 +140,8 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateCommentingRanges: Emitter<{ owner: string }> = this._register(new Emitter<{ owner: string }>()); readonly onDidUpdateCommentingRanges: Event<{ owner: string }> = this._onDidUpdateCommentingRanges.event; - private readonly _onDidChangeActiveCommentThread = this._register(new Emitter()); - readonly onDidChangeActiveCommentThread = this._onDidChangeActiveCommentThread.event; + private readonly _onDidChangeActiveEditingCommentThread = this._register(new Emitter()); + readonly onDidChangeActiveEditingCommentThread = this._onDidChangeActiveEditingCommentThread.event; private readonly _onDidChangeCurrentCommentThread = this._register(new Emitter()); readonly onDidChangeCurrentCommentThread = this._onDidChangeCurrentCommentThread.event; @@ -266,8 +268,18 @@ export class CommentService extends Disposable implements ICommentService { * The active comment thread is the the thread that is currently being edited. * @param commentThread */ - setActiveCommentThread(commentThread: CommentThread | null) { - this._onDidChangeActiveCommentThread.fire(commentThread); + setActiveEditingCommentThread(commentThread: CommentThread | null) { + this._onDidChangeActiveEditingCommentThread.fire(commentThread); + } + + async setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { + const commentController = this._commentControls.get(owner); + + if (!commentController) { + return; + } + + return commentController.setActiveCommentAndThread(commentInfo); } setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 035f9057c37..62656e6f97a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -60,7 +60,7 @@ export class CommentThreadBody extends D this._register(dom.addDisposableListener(container, dom.EventType.FOCUS_IN, e => { // TODO @rebornix, limit T to IRange | ICellRange - this.commentService.setActiveCommentThread(this._commentThread); + this.commentService.setActiveEditingCommentThread(this._commentThread); })); this._markdownRenderer = this._register(new MarkdownRenderer(this._options, this.languageService, this.openerService)); diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index de511db52f3..a3e171b9d40 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -68,6 +68,9 @@ class TestCommentController implements ICommentController { getNotebookComments(resource: URI, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + setActiveCommentAndThread(commentInfo: { thread: CommentThread; comment: Comment } | undefined): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0a1f29e9d7a..47486e60790 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -6,6 +6,7 @@ // THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. export const allApiProposals = Object.freeze({ + activeComment: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', aiRelatedInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', diff --git a/src/vscode-dts/vscode.proposed.activeComment.d.ts b/src/vscode-dts/vscode.proposed.activeComment.d.ts new file mode 100644 index 00000000000..f3e023c5bc5 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.activeComment.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // @alexr00 https://github.com/microsoft/vscode/issues/204484 + + export interface CommentController { + /** + * The currently active comment or `undefined`. The active comment is the one + * that currently has focus or, when none has focus, undefined. + */ + // readonly activeComment: Comment | undefined; + + /** + * The currently active comment thread or `undefined`. The active comment thread is the one + * that currently has focus or, when none has focus, undefined. + */ + readonly activeThread: CommentThread | undefined; + } +}