diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 82d7b1fc1e1..62c27c2d020 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1644,6 +1644,7 @@ export interface CommentThreadTemplate { export interface CommentInfo { extensionId?: string; threads: CommentThread[]; + pendingCommentThreads?: PendingCommentThread[]; commentingRanges: CommentingRanges; } @@ -1785,10 +1786,22 @@ export interface Comment { readonly timestamp?: string; } +export interface PendingCommentThread { + body: string; + range: IRange; + uri: URI; + owner: string; +} + /** * @internal */ export interface CommentThreadChangedEvent { + /** + * Pending comment threads. + */ + readonly pending: PendingCommentThread[]; + /** * Added comment threads. */ diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b3b99ebc23c..2a6218c8de5 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7622,6 +7622,13 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface PendingCommentThread { + body: string; + range: IRange; + uri: Uri; + owner: string; + } + export interface CodeLens { range: IRange; id?: string; diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index ab886d7dc8c..68455a45e42 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -7,7 +7,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import { IRange, Range } from 'vs/editor/common/core/range'; import * as languages from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -287,13 +286,15 @@ export class MainThreadCommentController implements ICommentController { this._commentService.updateComments(this._uniqueId, { added: [thread], removed: [], - changed: [] + changed: [], + pending: [] }); } else { this._commentService.updateNotebookComments(this._uniqueId, { added: [thread as MainThreadCommentThread], removed: [], - changed: [] + changed: [], + pending: [] }); } @@ -311,13 +312,15 @@ export class MainThreadCommentController implements ICommentController { this._commentService.updateComments(this._uniqueId, { added: [], removed: [], - changed: [thread] + changed: [thread], + pending: [] }); } else { this._commentService.updateNotebookComments(this._uniqueId, { added: [], removed: [], - changed: [thread as MainThreadCommentThread] + changed: [thread as MainThreadCommentThread], + pending: [] }); } @@ -332,13 +335,15 @@ export class MainThreadCommentController implements ICommentController { this._commentService.updateComments(this._uniqueId, { added: [], removed: [thread], - changed: [] + changed: [], + pending: [] }); } else { this._commentService.updateNotebookComments(this._uniqueId, { added: [], removed: [thread as MainThreadCommentThread], - changed: [] + changed: [], + pending: [] }); } } @@ -502,8 +507,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments })); } - $registerCommentController(handle: number, id: string, label: string): void { - const providerId = generateUuid(); + $registerCommentController(handle: number, id: string, label: string, extensionId: string): void { + const providerId = `${label}-${extensionId}`; this._handlers.set(handle, providerId); const provider = new MainThreadCommentController(this._proxy, this._commentService, handle, providerId, id, label, {}); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a3063e71595..eaf7745216b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -135,7 +135,7 @@ export type CommentThreadChanges = Partial<{ }>; export interface MainThreadCommentsShape extends IDisposable { - $registerCommentController(handle: number, id: string, label: string): void; + $registerCommentController(handle: number, id: string, label: string, extensionId: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean): languages.CommentThread | undefined; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index c1208be0537..f621a1d123c 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -592,7 +592,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _id: string, private _label: string ) { - proxy.$registerCommentController(this.handle, _id, _label); + proxy.$registerCommentController(this.handle, _id, _label, this._extension.identifier.value); const that = this; this.value = Object.freeze({ diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 6a8cc9c290c..2dbc44ada62 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread, CommentOptions } from 'vs/editor/common/languages'; +import { CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread, CommentOptions, PendingCommentThread } from 'vs/editor/common/languages'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Range, IRange } from 'vs/editor/common/core/range'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -18,6 +18,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export const ICommentService = createDecorator('commentService'); @@ -69,6 +70,10 @@ export interface ICommentController { getNotebookComments(resource: URI, token: CancellationToken): Promise; } +export interface IContinueOnCommentProvider { + provideContinueOnComments(): PendingCommentThread[]; +} + export interface ICommentService { readonly _serviceBrand: undefined; readonly onDidSetResourceCommentInfos: Event; @@ -103,8 +108,12 @@ export interface ICommentService { setActiveCommentThread(commentThread: CommentThread | null): void; setCurrentCommentThread(commentThread: CommentThread | undefined): void; enableCommenting(enable: boolean): void; + registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; + removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string }): PendingCommentThread | undefined; } +const CONTINUE_ON_COMMENTS = 'comments.continueOnComments'; + export class CommentService extends Disposable implements ICommentService { declare readonly _serviceBrand: undefined; @@ -152,16 +161,49 @@ export class CommentService extends Disposable implements ICommentService { private _isCommentingEnabled: boolean = true; private _workspaceHasCommenting: IContextKey; + private _continueOnComments = new Map(); // owner -> PendingCommentThread[] + private _continueOnCommentProviders = new Set(); + constructor( @IInstantiationService protected readonly instantiationService: IInstantiationService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService ) { super(); this._handleConfiguration(); this._handleZenMode(); this._workspaceHasCommenting = WorkspaceHasCommenting.bindTo(contextKeyService); + const storageListener = this._register(new DisposableStore()); + + storageListener.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, CONTINUE_ON_COMMENTS, storageListener)((v) => { + if (!v.external) { + return; + } + const commentsToRestore: PendingCommentThread[] | undefined = this.storageService.getObject(CONTINUE_ON_COMMENTS, StorageScope.WORKSPACE); + if (!commentsToRestore) { + return; + } + const changedOwners = this._addContinueOnComments(commentsToRestore); + for (const owner of changedOwners) { + const evt: ICommentThreadChangedEvent = { + owner, + pending: this._continueOnComments.get(owner) || [], + added: [], + removed: [], + changed: [] + }; + this._onDidUpdateCommentThreads.fire(evt); + } + })); + this._register(storageService.onWillSaveState(() => { + for (const provider of this._continueOnCommentProviders) { + const pendingComments = provider.provideContinueOnComments(); + this._addContinueOnComments(pendingComments); + } + this._saveContinueOnComments(); + })); } private _handleConfiguration() { @@ -325,12 +367,17 @@ export class CommentService extends Disposable implements ICommentService { async getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]> { const commentControlResult: Promise[] = []; - this._commentControls.forEach(control => { + for (const control of this._commentControls.values()) { commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None) + .then(documentComments => { + const pendingComments = this._continueOnComments.get(documentComments.owner); + documentComments.pendingCommentThreads = pendingComments?.filter(pendingComment => pendingComment.uri.toString() === resource.toString()); + return documentComments; + }) .catch(_ => { return null; })); - }); + } return Promise.all(commentControlResult); } @@ -347,4 +394,46 @@ export class CommentService extends Disposable implements ICommentService { return Promise.all(commentControlResult); } + + registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable { + this._continueOnCommentProviders.add(provider); + return { + dispose: () => { + this._continueOnCommentProviders.delete(provider); + } + }; + } + + private _saveContinueOnComments() { + const commentsToSave: PendingCommentThread[] = []; + for (const pendingComments of this._continueOnComments.values()) { + commentsToSave.push(...pendingComments); + } + this.storageService.store(CONTINUE_ON_COMMENTS, commentsToSave, StorageScope.WORKSPACE, StorageTarget.USER); + } + + removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string }): PendingCommentThread | undefined { + const pendingComments = this._continueOnComments.get(pendingComment.owner); + if (pendingComments) { + return pendingComments.splice(pendingComments.findIndex(comment => comment.uri.toString() === pendingComment.uri.toString() && Range.equalsRange(comment.range, pendingComment.range)), 1)[0]; + } + return undefined; + } + + private _addContinueOnComments(pendingComments: PendingCommentThread[]): Set { + const changedOwners = new Set(); + for (const pendingComment of pendingComments) { + if (!this._continueOnComments.has(pendingComment.owner)) { + this._continueOnComments.set(pendingComment.owner, [pendingComment]); + changedOwners.add(pendingComment.owner); + } else { + const commentsForOwner = this._continueOnComments.get(pendingComment.owner)!; + if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range) || (comment.body !== pendingComment.body))) { + commentsForOwner.push(pendingComment); + changedOwners.add(pendingComment.owner); + } + } + } + return changedOwners; + } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index de414ee854f..636de0344f6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -395,7 +395,39 @@ export class CommentController implements IEditorContribution { this.onModelChanged(); this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); - this.beginCompute(); + this.commentService.registerContinueOnCommentProvider({ + provideContinueOnComments: () => { + const pendingComments: languages.PendingCommentThread[] = []; + if (this._commentWidgets) { + for (const zone of this._commentWidgets) { + const zonePendingComments = zone.getPendingComments(); + const pendingNewComment = zonePendingComments.newComment; + if (!pendingNewComment || !zone.commentThread.range) { + continue; + } + let lastCommentBody; + if (zone.commentThread.comments && zone.commentThread.comments.length) { + const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1]; + if (typeof lastComment.body === 'string') { + lastCommentBody = lastComment.body; + } else { + lastCommentBody = lastComment.body.value; + } + } + + if (pendingNewComment !== lastCommentBody) { + pendingComments.push({ + owner: zone.owner, + uri: zone.editor.getModel()!.uri, + range: zone.commentThread.range, + body: pendingNewComment + }); + } + } + } + return pendingComments; + } + }); } private registerEditorListeners() { @@ -671,9 +703,10 @@ export class CommentController implements IEditorContribution { return; } - const added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); - const removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); - const changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); + const added = e.added.filter(thread => thread.resource && thread.resource === editorURI.toString()); + const removed = e.removed.filter(thread => thread.resource && thread.resource === editorURI.toString()); + const changed = e.changed.filter(thread => thread.resource && thread.resource === editorURI.toString()); + const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString()); removed.forEach(thread => { const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); @@ -713,12 +746,17 @@ export class CommentController implements IEditorContribution { return; } - const pendingCommentText = this._pendingNewCommentCache[e.owner] && this._pendingNewCommentCache[e.owner][thread.threadId!]; + const continueOnCommentText = (thread.range ? this.commentService.removeContinueOnComment({ owner: e.owner, uri: editorURI, range: thread.range })?.body : undefined); + const pendingCommentText = (this._pendingNewCommentCache[e.owner] && this._pendingNewCommentCache[e.owner][thread.threadId!]) + ?? continueOnCommentText; const pendingEdits = this._pendingEditsCache[e.owner] && this._pendingEditsCache[e.owner][thread.threadId!]; this.displayCommentThread(e.owner, thread, pendingCommentText, pendingEdits); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); this.tryUpdateReservedSpace(); }); + pending.forEach(thread => { + this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, Range.lift(thread.range)); + }); this._commentThreadRangeDecorator.update(this.editor, commentInfo); })); @@ -1002,6 +1040,9 @@ export class CommentController implements IEditorContribution { this.displayCommentThread(info.owner, thread, pendingComment, pendingEdits); }); + info.pendingCommentThreads?.forEach(thread => { + this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, Range.lift(thread.range)); + }); }); this._commentingRangeDecorator.update(this.editor, this._commentInfos);