diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 7f0dfde0e64..c6855a64798 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -484,6 +484,7 @@ export class CommentsList extends WorkbenchObjectTree('commentsView.hasComments', false); export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey('commentsView.commentFocused', false); const VIEW_STORAGE_ID = 'commentsViewState'; -function createResourceCommentsIterator(model: ICommentsModel): Iterable> { +type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode; + +function createResourceCommentsIterator(model: ICommentsModel): Iterable> { return Iterable.map(model.resourceCommentThreads, m => { const CommentNodeIt = Iterable.from(m.commentThreads); const children = Iterable.map(CommentNodeIt, r => ({ element: r })); @@ -161,6 +163,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.filters = this._register(new CommentsFilters({ showResolved: this.viewState['showResolved'] !== false, showUnresolved: this.viewState['showUnresolved'] !== false, + sortBy: this.viewState['sortBy'], }, this.contextKeyService)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); @@ -168,6 +171,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { if (event.showResolved || event.showUnresolved) { this.updateFilter(); } + if (event.sortBy) { + this.refresh(); + } })); this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter())); } @@ -177,6 +183,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.viewState['filterHistory'] = this.filterWidget.getHistory(); this.viewState['showResolved'] = this.filters.showResolved; this.viewState['showUnresolved'] = this.filters.showUnresolved; + this.viewState['sortBy'] = this.filters.sortBy; this.stateMemento.saveMemento(); super.saveState(); } @@ -270,10 +277,10 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private async renderComments(): Promise { + private renderComments(): void { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); - await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); } public collapseAll() { @@ -391,8 +398,16 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { overrideStyles: this.getLocationBasedColors().listOverrideStyles, selectionNavigation: true, filter: this.filter, + sorter: { + compare: (a: CommentsTreeNode, b: CommentsTreeNode) => { + if (this.filters.sortBy === CommentsSortOrder.UpdatedAtDescending && !(a instanceof CommentsModel) && !(b instanceof CommentsModel)) { + return a.lastUpdatedAt > b.lastUpdatedAt ? -1 : 1; + } + return 0; + }, + }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (item: CommentsModel | ResourceWithCommentThreads | CommentNode) => { + getKeyboardNavigationLabel: (item: CommentsTreeNode) => { return undefined; } }, @@ -449,11 +464,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } if (this.isVisible()) { this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads()); - - this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.cachedFilterStats = undefined; - this.renderMessage(); - this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.renderComments(); if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) { const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0]; diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index b850d68ac5c..05440049e5d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -10,24 +10,34 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { viewFilterSubmenu } from 'vs/workbench/browser/parts/views/viewFilter'; +import { Codicon } from 'vs/base/common/codicons'; + +export const enum CommentsSortOrder { + ResourceAscending = 'resourceAscending', + UpdatedAtDescending = 'updatedAtDescending', +} + const CONTEXT_KEY_SHOW_RESOLVED = new RawContextKey('commentsView.showResolvedFilter', true); const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey('commentsView.showUnResolvedFilter', true); +const CONTEXT_KEY_SORT_BY = new RawContextKey('commentsView.sortBy', CommentsSortOrder.ResourceAscending); export interface CommentsFiltersChangeEvent { showResolved?: boolean; showUnresolved?: boolean; + sortBy?: CommentsSortOrder; } interface CommentsFiltersOptions { showResolved: boolean; showUnresolved: boolean; + sortBy: CommentsSortOrder; } export class CommentsFilters extends Disposable { @@ -39,6 +49,7 @@ export class CommentsFilters extends Disposable { super(); this._showResolved.set(options.showResolved); this._showUnresolved.set(options.showUnresolved); + this._sortBy.set(options.sortBy); } private readonly _showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService); @@ -63,6 +74,16 @@ export class CommentsFilters extends Disposable { } } + private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); + get sortBy(): CommentsSortOrder { + return this._sortBy.get()!; + } + set sortBy(sortBy: CommentsSortOrder) { + if (this._sortBy.get() !== sortBy) { + this._sortBy.set(sortBy); + this._onDidChange.fire({ sortBy }); + } + } } registerAction2(class extends ViewAction { @@ -168,3 +189,40 @@ registerAction2(class extends ViewAction { view.filters.showResolved = !view.filters.showResolved; } }); + +const commentSortSubmenu = new MenuId('submenu.filter.commentSort'); +MenuRegistry.appendMenuItem(viewFilterSubmenu, { + submenu: commentSortSubmenu, + title: localize('comment sorts', "Sort By"), + group: '2_sort', + icon: Codicon.history, + when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID), +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByUpdatedAt`, + title: localize('toggle sorting by updated at', "Updated Time"), + category: localize('comments', "Comments"), + icon: Codicon.history, + viewId: COMMENTS_VIEW_ID, + toggled: { + condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending), + title: localize('sorting by updated at', "Updated Time"), + }, + menu: { + id: commentSortSubmenu, + group: 'navigation', + order: 0, + isHiddenByDefault: false, + }, + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise { + view.filters.sortBy = view.filters.sortBy === CommentsSortOrder.ResourceAscending + ? CommentsSortOrder.UpdatedAtDescending + : CommentsSortOrder.ResourceAscending; + } +}); diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index fbf25f6c06b..716985b311b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -42,6 +42,23 @@ export class CommentNode { hasReply(): boolean { return this.replies && this.replies.length !== 0; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt(): string { + if (this._lastUpdatedAt === undefined) { + let updatedAt = this.comment.timestamp || ''; + if (this.replies.length) { + const reply = this.replies[this.replies.length - 1]; + const replyUpdatedAt = reply.lastUpdatedAt; + if (replyUpdatedAt > updatedAt) { + updatedAt = replyUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } } export class ResourceWithCommentThreads { @@ -71,5 +88,25 @@ export class ResourceWithCommentThreads { return commentNodes[0]; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt() { + if (this._lastUpdatedAt === undefined) { + let updatedAt = ''; + // Return result without cahcing as we expect data to arrive later + if (!this.commentThreads.length) { + return updatedAt; + } + for (const thread of this.commentThreads) { + const threadUpdatedAt = thread.lastUpdatedAt; + if (threadUpdatedAt && threadUpdatedAt > updatedAt) { + updatedAt = threadUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } }