Add "sort by updated at" option to comments panel (#221665)

* feat: add "sort by updated at" to comments panel

Fixes #149449

* fix: sort comments panel using ObjectTree sorter

* fix: address PR review suggestions

* Move sorting back into filter menu
and fix nits

---------

Co-authored-by: Alex Ross <alros@microsoft.com>
This commit is contained in:
Victor Hallberg
2024-07-30 12:28:39 +02:00
committed by GitHub
parent 447e8dcee6
commit bb02803698
4 changed files with 120 additions and 12 deletions
@@ -484,6 +484,7 @@ export class CommentsList extends WorkbenchObjectTree<CommentsModel | ResourceWi
collapseByDefault: false,
overrideStyles: options.overrideStyles,
filter: options.filter,
sorter: options.sorter,
findWidgetEnabled: false,
multipleSelectionSupport: false,
},
@@ -24,26 +24,28 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
import { CommentsFilters, CommentsFiltersChangeEvent, CommentsSortOrder } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions';
import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages';
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { Iterable } from 'vs/base/common/iterator';
import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController';
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
import { CommentsModel, type ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
import { IHoverService } from 'vs/platform/hover/browser/hover';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
import type { ITreeElement } from 'vs/base/browser/ui/tree/tree';
export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey<boolean>('commentsView.commentFocused', false);
const VIEW_STORAGE_ID = 'commentsViewState';
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<ResourceWithCommentThreads | CommentNode>> {
type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode;
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<CommentsTreeNode>> {
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<void> {
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];
@@ -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<boolean>('commentsView.showResolvedFilter', true);
const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey<boolean>('commentsView.showUnResolvedFilter', true);
const CONTEXT_KEY_SORT_BY = new RawContextKey<CommentsSortOrder>('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<ICommentsView> {
@@ -168,3 +189,40 @@ registerAction2(class extends ViewAction<ICommentsView> {
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<ICommentsView> {
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<void> {
view.filters.sortBy = view.filters.sortBy === CommentsSortOrder.ResourceAscending
? CommentsSortOrder.UpdatedAtDescending
: CommentsSortOrder.ResourceAscending;
}
});
@@ -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;
}
}