Skip to content

Commit 2c93c17

Browse files
authored
Add flitering for comments (microsoft#158264)
* wip * Filter by resolved/unresolved * Text filtering * Get filter count working * Add keyboard shortcuts * Badge and no-threadState fixing
1 parent b0d8e1a commit 2c93c17

File tree

6 files changed

+829
-12
lines changed

6 files changed

+829
-12
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Event } from 'vs/base/common/event';
7+
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
8+
import { IView } from 'vs/workbench/common/views';
9+
import { CommentsFilters } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
10+
11+
export const CommentsViewFilterFocusContextKey = new RawContextKey<boolean>('commentsFilterFocus', false);
12+
export const CommentsViewSmallLayoutContextKey = new RawContextKey<boolean>(`commentsView.smallLayout`, false);
13+
14+
export interface ICommentsView extends IView {
15+
16+
readonly onDidFocusFilter: Event<void>;
17+
readonly onDidClearFilterText: Event<void>;
18+
readonly filters: CommentsFilters;
19+
readonly onDidChangeFilterStats: Event<{ total: number; filtered: number }>;
20+
focusFilter(): void;
21+
clearFilterText(): void;
22+
getFilterStats(): { total: number; filtered: number };
23+
24+
collapseAll(): void;
25+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IFilter, matchesFuzzy, matchesFuzzy2 } from 'vs/base/common/filters';
7+
import * as strings from 'vs/base/common/strings';
8+
9+
export class FilterOptions {
10+
11+
static readonly _filter: IFilter = matchesFuzzy2;
12+
static readonly _messageFilter: IFilter = matchesFuzzy;
13+
14+
readonly showResolved: boolean = true;
15+
readonly showUnresolved: boolean = true;
16+
readonly textFilter: { readonly text: string; readonly negate: boolean };
17+
18+
constructor(
19+
readonly filter: string,
20+
showResolved: boolean,
21+
showUnresolved: boolean,
22+
) {
23+
filter = filter.trim();
24+
this.showResolved = showResolved;
25+
this.showUnresolved = showUnresolved;
26+
27+
const negate = filter.startsWith('!');
28+
this.textFilter = { text: (negate ? strings.ltrim(filter, '!') : filter).trim(), negate };
29+
}
30+
}

src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
1111
import { IOpenerService } from 'vs/platform/opener/common/opener';
1212
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
1313
import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
14-
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
14+
import { IAsyncDataSource, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
1515
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
1616
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1717
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@@ -25,6 +25,9 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
2525
import { commentViewThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors';
2626
import { CommentThreadState } from 'vs/editor/common/languages';
2727
import { Color } from 'vs/base/common/color';
28+
import { IMatch } from 'vs/base/common/filters';
29+
import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions';
30+
import { basename } from 'vs/base/common/resources';
2831

2932
export const COMMENTS_VIEW_ID = 'workbench.panel.comments';
3033
export const COMMENTS_VIEW_STORAGE_ID = 'Comments';
@@ -70,7 +73,7 @@ interface ICommentThreadTemplateData {
7073
disposables: IDisposable[];
7174
}
7275

73-
export class CommentsModelVirualDelegate implements IListVirtualDelegate<any> {
76+
export class CommentsModelVirualDelegate implements IListVirtualDelegate<ResourceWithCommentThreads | CommentNode> {
7477
private static readonly RESOURCE_ID = 'resource-with-comments';
7578
private static readonly COMMENT_ID = 'comment-node';
7679

@@ -253,7 +256,81 @@ export interface ICommentsListOptions extends IWorkbenchAsyncDataTreeOptions<any
253256
overrideStyles?: IColorMapping;
254257
}
255258

256-
export class CommentsList extends WorkbenchAsyncDataTree<any, any> {
259+
const enum FilterDataType {
260+
Resource,
261+
Comment
262+
}
263+
264+
interface ResourceFilterData {
265+
type: FilterDataType.Resource;
266+
uriMatches: IMatch[];
267+
}
268+
269+
interface CommentFilterData {
270+
type: FilterDataType.Comment;
271+
textMatches: IMatch[];
272+
}
273+
274+
export type FilterData = ResourceFilterData | CommentFilterData;
275+
276+
export class Filter implements ITreeFilter<ResourceWithCommentThreads | CommentNode, FilterData> {
277+
278+
constructor(public options: FilterOptions) { }
279+
280+
filter(element: ResourceWithCommentThreads | CommentNode, parentVisibility: TreeVisibility): TreeFilterResult<FilterData> {
281+
if (element instanceof ResourceWithCommentThreads) {
282+
return this.filterResourceMarkers(element);
283+
} else {
284+
return this.filterCommentNode(element, parentVisibility);
285+
}
286+
}
287+
288+
private filterResourceMarkers(resourceMarkers: ResourceWithCommentThreads): TreeFilterResult<FilterData> {
289+
// Filter by text. Do not apply negated filters on resources instead use exclude patterns
290+
if (this.options.textFilter.text && !this.options.textFilter.negate) {
291+
const uriMatches = FilterOptions._filter(this.options.textFilter.text, basename(resourceMarkers.resource));
292+
if (uriMatches) {
293+
return { visibility: true, data: { type: FilterDataType.Resource, uriMatches: uriMatches || [] } };
294+
}
295+
}
296+
297+
return TreeVisibility.Recurse;
298+
}
299+
300+
private filterCommentNode(comment: CommentNode, parentVisibility: TreeVisibility): TreeFilterResult<FilterData> {
301+
const matchesResolvedState = (comment.threadState === undefined) || (this.options.showResolved && CommentThreadState.Resolved === comment.threadState) ||
302+
(this.options.showUnresolved && CommentThreadState.Unresolved === comment.threadState);
303+
304+
if (!matchesResolvedState) {
305+
return false;
306+
}
307+
308+
if (!this.options.textFilter.text) {
309+
return true;
310+
}
311+
312+
const textMatches = FilterOptions._messageFilter(this.options.textFilter.text, typeof comment.comment.body === 'string' ? comment.comment.body : comment.comment.body.value);
313+
314+
// Matched and not negated
315+
if (textMatches && !this.options.textFilter.negate) {
316+
return { visibility: true, data: { type: FilterDataType.Comment, textMatches } };
317+
}
318+
319+
// Matched and negated - exclude it only if parent visibility is not set
320+
if (textMatches && this.options.textFilter.negate && parentVisibility === TreeVisibility.Recurse) {
321+
return false;
322+
}
323+
324+
// Not matched and negated - include it only if parent visibility is not set
325+
if (!textMatches && this.options.textFilter.negate && parentVisibility === TreeVisibility.Recurse) {
326+
return true;
327+
}
328+
329+
return parentVisibility;
330+
}
331+
}
332+
333+
export class CommentsList extends WorkbenchAsyncDataTree<CommentsModel | ResourceWithCommentThreads | CommentNode, any> {
257334
constructor(
258335
labels: ResourceLabels,
259336
container: HTMLElement,
@@ -304,7 +381,9 @@ export class CommentsList extends WorkbenchAsyncDataTree<any, any> {
304381
collapseByDefault: () => {
305382
return false;
306383
},
307-
overrideStyles: options.overrideStyles
384+
overrideStyles: options.overrideStyles,
385+
filter: options.filter,
386+
findWidgetEnabled: false
308387
},
309388
instantiationService,
310389
contextKeyService,
@@ -313,4 +392,23 @@ export class CommentsList extends WorkbenchAsyncDataTree<any, any> {
313392
configurationService
314393
);
315394
}
395+
396+
filterComments(): void {
397+
this.refilter();
398+
}
399+
400+
getVisibleItemCount(): number {
401+
let filtered = 0;
402+
const root = this.getNode();
403+
404+
for (const resourceNode of root.children) {
405+
for (const commentNode of resourceNode.children) {
406+
if (commentNode.visible && resourceNode.visible) {
407+
filtered++;
408+
}
409+
}
410+
}
411+
412+
return filtered;
413+
}
316414
}

0 commit comments

Comments
 (0)