promote filter action to view pane (#163132)

promote filter to view pane
This commit is contained in:
Sandeep Somavarapu 2022-10-10 09:01:28 +02:00 committed by GitHub
parent 6483e39fb7
commit bc647873e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 786 additions and 1303 deletions

View file

@ -140,6 +140,10 @@ export class ToolBar extends Disposable {
return this.element;
}
focus(): void {
this.actionBar.focus();
}
getItemsWidth(): number {
let itemsWidth = 0;
for (let i = 0; i < this.actionBar.length(); i++) {

View file

@ -264,7 +264,7 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar {
super(container, { resetMenu: menuId, ...options }, menuService, contextKeyService, contextMenuService, keybindingService, telemetryService);
// update logic
const menu = this._store.add(menuService.createMenu(menuId, contextKeyService));
const menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true }));
const updateToolbar = () => {
const primary: IAction[] = [];
const secondary: IAction[] = [];

View file

@ -20,6 +20,7 @@ import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/view
import { IPaneComposite } from 'vs/workbench/common/panecomposite';
import { IView } from 'vs/workbench/common/views';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { VIEWPANE_FILTER_ACTION } from 'vs/workbench/browser/parts/views/viewPane';
export abstract class PaneComposite extends Composite implements IPaneComposite {
@ -89,7 +90,11 @@ export abstract class PaneComposite extends Composite implements IPaneComposite
if (this.viewPaneContainer?.menuActions) {
result.push(...this.viewPaneContainer.menuActions.getPrimaryActions());
if (this.viewPaneContainer.isViewMergedWithContainer()) {
result.push(...this.viewPaneContainer.panes[0].menuActions.getPrimaryActions());
const viewPane = this.viewPaneContainer.panes[0];
if (viewPane.shouldShowFilterInHeader()) {
result.push(VIEWPANE_FILTER_ACTION);
}
result.push(...viewPane.menuActions.getPrimaryActions());
}
}
return result;

View file

@ -201,3 +201,76 @@
.customview-tree .monaco-list .monaco-list-row.focused .custom-view-tree-node-item .actions {
display: block;
}
/* filter view pane */
.viewpane-filter-container {
cursor: default;
display: flex;
}
.viewpane-filter-container.grow {
flex: 1;
}
.viewpane-filter-container > .viewpane-filter {
display: flex;
align-items: center;
flex: 1;
position: relative;
}
.viewpane-filter-container > .viewpane-filter .monaco-inputbox {
height: 24px;
font-size: 12px;
flex: 1;
}
.pane-header .viewpane-filter-container > .viewpane-filter .monaco-inputbox .monaco-inputbox {
height: 20px;
line-height: 18px;
}
.monaco-workbench.vs .viewpane-filter-container > .viewpane-filter .monaco-inputbox {
height: 25px;
}
.viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls {
position: absolute;
top: 0px;
bottom: 0;
right: 0px;
display: flex;
align-items: center;
}
.viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge {
margin: 4px 0px;
padding: 0px 8px;
border-radius: 2px;
}
.viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge.hidden,
.viewpane-filter.small > .viewpane-filter-controls > .viewpane-filter-badge {
display: none;
}
.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter {
padding: 2px;
}
.panel > .title .monaco-action-bar .action-item.viewpane-filter-container {
max-width: 400px;
min-width: 300px;
margin-right: 10px;
}
.pane-body .viewpane-filter-container:not(:empty) {
flex: 1;
margin: 10px 20px;
height: initial;
}
.pane-body .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls .monaco-action-bar .action-item {
margin-right: 4px;
}

View file

@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer } from 'vs/base/common/async';
import * as DOM from 'vs/base/browser/dom';
import { IAction } from 'vs/base/common/actions';
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { toDisposable } from 'vs/base/common/lifecycle';
import { badgeBackground, badgeForeground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ContextScopedHistoryInputBox } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Codicon } from 'vs/base/common/codicons';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint';
import { MenuId, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { Widget } from 'vs/base/browser/ui/widget';
import { Emitter } from 'vs/base/common/event';
const viewFilterMenu = new MenuId('menu.view.filter');
export const viewFilterSubmenu = new MenuId('submenu.view.filter');
MenuRegistry.appendMenuItem(viewFilterMenu, {
submenu: viewFilterSubmenu,
title: localize('more filters', "More Filters..."),
group: 'navigation',
icon: Codicon.filter,
});
class MoreFiltersActionViewItem extends SubmenuEntryActionViewItem {
private _checked: boolean = false;
set checked(checked: boolean) {
if (this._checked !== checked) {
this._checked = checked;
this.updateChecked();
}
}
protected override updateChecked(): void {
if (this.element) {
this.element.classList.toggle('checked', this._checked);
}
}
override render(container: HTMLElement): void {
super.render(container);
this.updateChecked();
}
}
export interface IFilterWidgetOptions {
readonly text?: string;
readonly placeholder?: string;
readonly ariaLabel?: string;
readonly history?: string[];
readonly focusContextKey?: string;
}
export class FilterWidget extends Widget {
readonly element: HTMLElement;
private readonly delayedFilterUpdate: Delayer<void>;
private readonly filterInputBox: HistoryInputBox;
private readonly filterBadge: HTMLElement;
private readonly toolbar: MenuWorkbenchToolBar;
private readonly focusContextKey: IContextKey<boolean> | undefined;
private readonly _onDidChangeFilterText = this._register(new Emitter<string>());
readonly onDidChangeFilterText = this._onDidChangeFilterText.event;
private moreFiltersActionViewItem: MoreFiltersActionViewItem | undefined;
private isMoreFiltersChecked: boolean = false;
constructor(
private readonly options: IFilterWidgetOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService private readonly keybindingService: IKeybindingService
) {
super();
this.delayedFilterUpdate = new Delayer<void>(400);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
if (options.focusContextKey) {
this.focusContextKey = new RawContextKey(options.focusContextKey, false).bindTo(contextKeyService);
}
this.element = DOM.$('.viewpane-filter');
this.filterInputBox = this.createInput(this.element);
const controlsContainer = DOM.append(this.element, DOM.$('.viewpane-filter-controls'));
this.filterBadge = this.createBadge(controlsContainer);
this.toolbar = this._register(this.createToolBar(controlsContainer));
this.adjustInputBox();
}
focus(): void {
this.filterInputBox.focus();
}
blur(): void {
this.filterInputBox.blur();
}
updateBadge(message: string | undefined): void {
this.filterBadge.classList.toggle('hidden', !message);
this.filterBadge.textContent = message || '';
this.adjustInputBox();
}
setFilterText(filterText: string): void {
this.filterInputBox.value = filterText;
}
getFilterText(): string {
return this.filterInputBox.value;
}
getHistory(): string[] {
return this.filterInputBox.getHistory();
}
layout(width: number): void {
this.element.parentElement?.classList.toggle('grow', width > 700);
this.element.classList.toggle('small', width < 400);
this.adjustInputBox();
}
checkMoreFilters(checked: boolean): void {
this.isMoreFiltersChecked = checked;
if (this.moreFiltersActionViewItem) {
this.moreFiltersActionViewItem.checked = checked;
}
}
private createInput(container: HTMLElement): ContextScopedHistoryInputBox {
const inputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: this.options.placeholder,
ariaLabel: this.options.ariaLabel,
history: this.options.history || [],
showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService)
}));
this._register(attachInputBoxStyler(inputBox, this.themeService));
if (this.options.text) {
inputBox.value = this.options.text;
}
this._register(inputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(inputBox!))));
this._register(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, inputBox!)));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.CLICK, (e) => {
e.stopPropagation();
e.preventDefault();
}));
const focusTracker = this._register(DOM.trackFocus(inputBox.inputElement));
if (this.focusContextKey) {
this._register(focusTracker.onDidFocus(() => this.focusContextKey!.set(true)));
this._register(focusTracker.onDidBlur(() => this.focusContextKey!.set(false)));
this._register(toDisposable(() => this.focusContextKey!.reset()));
}
return inputBox;
}
private createBadge(container: HTMLElement): HTMLElement {
const filterBadge = DOM.append(container, DOM.$('.viewpane-filter-badge.hidden'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
filterBadge.style.backgroundColor = background;
filterBadge.style.borderWidth = border ? '1px' : '';
filterBadge.style.borderStyle = border ? 'solid' : '';
filterBadge.style.borderColor = border;
filterBadge.style.color = foreground;
}));
return filterBadge;
}
private createToolBar(container: HTMLElement): MenuWorkbenchToolBar {
return this.instantiationService.createInstance(MenuWorkbenchToolBar, container, viewFilterMenu,
{
hiddenItemStrategy: HiddenItemStrategy.NoHide,
actionViewItemProvider: (action: IAction) => {
if (action instanceof SubmenuItemAction && action.item.submenu.id === viewFilterSubmenu.id) {
this.moreFiltersActionViewItem = this.instantiationService.createInstance(MoreFiltersActionViewItem, action, undefined);
this.moreFiltersActionViewItem.checked = this.isMoreFiltersChecked;
return this.moreFiltersActionViewItem;
}
return undefined;
}
});
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this._onDidChangeFilterText.fire(inputbox.value);
}
private adjustInputBox(): void {
this.filterInputBox.inputElement.style.paddingRight = this.element.classList.contains('small') || this.filterBadge.classList.contains('hidden') ? '25px' : '150px';
}
// Action toolbar is swallowing some keys for action items which should not be for an input box
private handleKeyboardEvent(event: StandardKeyboardEvent) {
if (event.equals(KeyCode.Space)
|| event.equals(KeyCode.LeftArrow)
|| event.equals(KeyCode.RightArrow)
) {
event.stopPropagation();
}
}
private onInputKeyDown(event: StandardKeyboardEvent, filterInputBox: HistoryInputBox) {
let handled = false;
if (event.equals(KeyCode.Tab)) {
this.toolbar.focus();
handled = true;
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const inputActiveOptionBorderColor = theme.getColor(inputActiveOptionBorder);
if (inputActiveOptionBorderColor) {
collector.addRule(`.viewpane-filter > .viewpane-filter-controls .monaco-action-bar .action-label.codicon.codicon-filter.checked { border-color: ${inputActiveOptionBorderColor}; }`);
}
const inputActiveOptionForegroundColor = theme.getColor(inputActiveOptionForeground);
if (inputActiveOptionForegroundColor) {
collector.addRule(`.viewpane-filter > .viewpane-filter-controls .monaco-action-bar .action-label.codicon.codicon-filter.checked { color: ${inputActiveOptionForegroundColor}; }`);
}
const inputActiveOptionBackgroundColor = theme.getColor(inputActiveOptionBackground);
if (inputActiveOptionBackgroundColor) {
collector.addRule(`.viewpane-filter > .viewpane-filter-controls .monaco-action-bar .action-label.codicon.codicon-filter.checked { background-color: ${inputActiveOptionBackgroundColor}; }`);
}
});

View file

@ -9,9 +9,9 @@ import { Event, Emitter } from 'vs/base/common/event';
import { foreground } from 'vs/platform/theme/common/colorRegistry';
import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset } from 'vs/base/browser/dom';
import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar';
import { Registry } from 'vs/platform/registry/common/platform';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@ -42,6 +42,9 @@ import { Codicon } from 'vs/base/common/codicons';
import { CompositeMenuActions } from 'vs/workbench/browser/actions';
import { IDropdownMenuActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { FilterWidget, IFilterWidgetOptions } from 'vs/workbench/browser/parts/views/viewFilter';
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
export interface IViewPaneOptions extends IPaneOptions {
id: string;
@ -50,6 +53,12 @@ export interface IViewPaneOptions extends IPaneOptions {
donotForwardArgs?: boolean;
}
export interface IFilterViewPaneOptions extends IViewPaneOptions {
filterOptions: IFilterWidgetOptions;
}
export const VIEWPANE_FILTER_ACTION = new Action('viewpane.action.filter');
type WelcomeActionClassification = {
owner: 'joaomoreno';
viewId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The view ID in which the welcome view button was clicked.' };
@ -501,7 +510,11 @@ export abstract class ViewPane extends Pane implements IView {
private setActions(): void {
if (this.toolbar) {
this.toolbar.setActions(prepareActions(this.menuActions.getPrimaryActions()), prepareActions(this.menuActions.getSecondaryActions()));
const primaryActions = [...this.menuActions.getPrimaryActions()];
if (this.shouldShowFilterInHeader()) {
primaryActions.unshift(VIEWPANE_FILTER_ACTION);
}
this.toolbar.setActions(prepareActions(primaryActions), prepareActions(this.menuActions.getSecondaryActions()));
this.toolbar.context = this.getActionsContext();
}
}
@ -520,6 +533,18 @@ export abstract class ViewPane extends Pane implements IView {
}
getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
if (action.id === VIEWPANE_FILTER_ACTION.id) {
const that = this;
return new class extends BaseActionViewItem {
constructor() { super(null, action); }
override setFocusable(): void { /* noop input elements are focusable by default */ }
override get trapsArrowNavigation(): boolean { return true; }
override render(container: HTMLElement): void {
container.classList.add('viewpane-filter-container');
append(container, that.getFilterWidget()!.element);
}
};
}
return createActionViewItem(this.instantiationService, action, { ...options, ...{ menuAsChild: action instanceof SubmenuItemAction } });
}
@ -626,6 +651,75 @@ export abstract class ViewPane extends Pane implements IView {
shouldShowWelcome(): boolean {
return false;
}
getFilterWidget(): FilterWidget | undefined {
return undefined;
}
shouldShowFilterInHeader(): boolean {
return false;
}
}
export abstract class FilterViewPane extends ViewPane {
readonly filterWidget: FilterWidget;
private dimension: Dimension | undefined;
private filterContainer: HTMLElement | undefined;
constructor(
options: IFilterViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
const scopedContextKeyService = this._register(contextKeyService.createScoped(this.element));
scopedContextKeyService.createKey('view', options.id);
this.filterWidget = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])).createInstance(FilterWidget, options.filterOptions));
}
override getFilterWidget(): FilterWidget {
return this.filterWidget;
}
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
this.filterContainer = append(container, $('.viewpane-filter-container'));
}
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
this.dimension = new Dimension(width, height);
const wasFilterShownInHeader = !this.filterContainer?.hasChildNodes();
const shouldShowFilterInHeader = this.shouldShowFilterInHeader();
if (wasFilterShownInHeader !== shouldShowFilterInHeader) {
if (shouldShowFilterInHeader) {
reset(this.filterContainer!);
}
this.updateActions();
if (!shouldShowFilterInHeader) {
append(this.filterContainer!, this.filterWidget.element);
height = height - 44;
}
}
this.filterWidget.layout(width);
this.layoutBodyContent(height, width);
}
override shouldShowFilterInHeader(): boolean {
return !(this.dimension && this.dimension.width < 600 && this.dimension.height > 100);
}
protected abstract layoutBodyContent(height: number, width: number): void;
}
export abstract class ViewAction<T extends IView> extends Action2 {

View file

@ -3,20 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IView } from 'vs/workbench/common/views';
import { CommentsFilters } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
export const CommentsViewFilterFocusContextKey = new RawContextKey<boolean>('commentsFilterFocus', false);
export const CommentsViewSmallLayoutContextKey = new RawContextKey<boolean>(`commentsView.smallLayout`, false);
export interface ICommentsView extends IView {
readonly onDidFocusFilter: Event<void>;
readonly onDidClearFilterText: Event<void>;
readonly filters: CommentsFilters;
readonly onDidChangeFilterStats: Event<{ total: number; filtered: number }>;
focusFilter(): void;
clearFilterText(): void;
getFilterStats(): { total: number; filtered: number };

View file

@ -18,7 +18,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry';
import { ResourceLabels } from 'vs/workbench/browser/labels';
import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewPaneOptions, ViewAction, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
@ -31,11 +31,8 @@ import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IEditor } from 'vs/editor/common/editorCommon';
import { TextModel } from 'vs/editor/common/model/textModel';
import { Action, IAction } from 'vs/base/common/actions';
import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { CommentsViewSmallLayoutContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { CommentsFilterActionViewItem, CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
import { Event, Emitter } from 'vs/base/common/event';
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { CommentsFilters, CommentsFiltersChangeEvent } 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';
@ -43,7 +40,7 @@ import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFil
const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
const VIEW_STORAGE_ID = 'commentsViewState';
export class CommentsPanel extends ViewPane implements ICommentsView {
export class CommentsPanel extends FilterViewPane implements ICommentsView {
private treeLabels!: ResourceLabels;
private tree: CommentsList | undefined;
private treeContainer!: HTMLElement;
@ -51,10 +48,8 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
private commentsModel!: CommentsModel;
private totalComments: number = 0;
private readonly hasCommentsContextKey: IContextKey<boolean>;
private readonly smallLayoutContextKey: IContextKey<boolean>;
private readonly filter: Filter;
readonly filters: CommentsFilters;
private filterActionBar: ActionBar | undefined;
private currentHeight = 0;
private currentWidth = 0;
@ -64,13 +59,6 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
readonly onDidChangeVisibility = this.onDidChangeBodyVisibility;
private readonly _onDidFocusFilter: Emitter<void> = this._register(new Emitter<void>());
readonly onDidFocusFilter: Event<void> = this._onDidFocusFilter.event;
private readonly _onDidClearFilterText: Emitter<void> = this._register(new Emitter<void>());
readonly onDidClearFilterText: Event<void> = this._onDidClearFilterText.event;
private _onDidChangeFilterStats = this._register(new Emitter<{ total: number; filtered: number }>());
readonly onDidChangeFilterStats: Event<{ total: number; filtered: number }> = this._onDidChangeFilterStats.event;
constructor(
options: IViewPaneOptions,
@IInstantiationService instantiationService: IInstantiationService,
@ -87,20 +75,27 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IStorageService readonly storageService: IStorageService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
const stateMemento = new Memento(VIEW_STORAGE_ID, storageService);
const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
super({
...options,
filterOptions: {
placeholder: nls.localize('comments.filter.placeholder', "Filter (e.g. text, author)"),
ariaLabel: nls.localize('comments.filter.ariaLabel', "Filter comments"),
history: viewState['filterHistory'] || [],
text: viewState['filter'] || '',
focusContextKey: CommentsViewFilterFocusContextKey.key
}
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService);
this.smallLayoutContextKey = CommentsViewSmallLayoutContextKey.bindTo(this.contextKeyService);
this.stateMemento = new Memento(VIEW_STORAGE_ID, storageService);
this.viewState = this.stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
this.stateMemento = stateMemento;
this.viewState = viewState;
this.filters = this._register(new CommentsFilters({
filterText: this.viewState['filter'] || '',
filterHistory: this.viewState['filterHistory'] || [],
showResolved: this.viewState['showResolved'] !== false,
showUnresolved: this.viewState['showUnresolved'] !== false,
layout: new dom.Dimension(0, 0)
}));
this.filter = new Filter(new FilterOptions(this.filters.filterText, this.filters.showResolved, this.filters.showUnresolved));
}, this.contextKeyService));
this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved));
this._register(this.commentService.onDidSetAllCommentThreads(e => {
this.totalComments = e.commentThreads.length;
@ -112,15 +107,16 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
}));
this._register(this.filters.onDidChange((event: CommentsFiltersChangeEvent) => {
if (event.filterText || event.showResolved || event.showUnresolved) {
if (event.showResolved || event.showUnresolved) {
this.updateFilter();
}
}));
this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter()));
}
override saveState(): void {
this.viewState['filter'] = this.filters.filterText;
this.viewState['filterHistory'] = this.filters.filterHistory;
this.viewState['filter'] = this.filterWidget.getFilterText();
this.viewState['filterHistory'] = this.filterWidget.getHistory();
this.viewState['showResolved'] = this.filters.showResolved;
this.viewState['showUnresolved'] = this.filters.showUnresolved;
this.stateMemento.saveMemento();
@ -128,11 +124,11 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
}
public focusFilter(): void {
this._onDidFocusFilter.fire();
this.filterWidget.focus();
}
public clearFilterText(): void {
this._onDidClearFilterText.fire();
this.filterWidget.setFilterText('');
}
public getFilterStats(): { total: number; filtered: number } {
@ -147,30 +143,21 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
}
private updateFilter() {
this.filter.options = new FilterOptions(this.filters.filterText, this.filters.showResolved, this.filters.showUnresolved);
this.filter.options = new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved);
this.tree?.filterComments();
this.cachedFilterStats = undefined;
this._onDidChangeFilterStats.fire(this.getFilterStats());
const { total, filtered } = this.getFilterStats();
this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : nls.localize('showing filtered results', "Showing {0} of {1}", filtered, total));
this.filterWidget.checkMoreFilters(!this.filters.showResolved || !this.filters.showUnresolved);
}
private createFilterActionBar(parent: HTMLElement): void {
this.filterActionBar = this._register(new ActionBar(parent, { actionViewItemProvider: action => this.getActionViewItem(action) }));
this.filterActionBar.getContainer().classList.add('comments-panel-filter-container');
this.filterActionBar.getContainer().classList.toggle('hide', !this.smallLayout);
}
private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); }
private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); }
public override renderBody(container: HTMLElement): void {
super.renderBody(container);
container.classList.add('comments-panel');
const domContainer = dom.append(container, dom.$('.comments-panel-container'));
this.createFilterActionBar(domContainer);
this.filterActionBar!.push(new Action(`workbench.actions.treeView.${this.id}.filter`));
this.treeContainer = dom.append(domContainer, dom.$('.tree-container'));
this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
@ -255,20 +242,11 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
return !!this.tree;
}
public override layoutBody(height: number = this.currentHeight, width: number = this.currentWidth): void {
super.layoutBody(height, width);
const wasSmallLayout = this.smallLayout;
this.smallLayout = width < 600 && height > 100;
if (this.smallLayout !== wasSmallLayout) {
this.filterActionBar?.getContainer().classList.toggle('hide', !this.smallLayout);
}
const contentHeight = this.smallLayout ? height - 44 : height;
public override layoutBodyContent(height: number = this.currentHeight, width: number = this.currentWidth): void {
if (this.messageBoxContainer) {
this.messageBoxContainer.style.height = `${contentHeight}px`;
this.messageBoxContainer.style.height = `${height}px`;
}
this.tree?.layout(contentHeight, width);
this.filters.layout = new dom.Dimension(this.smallLayout ? width : width - 200, height);
this.tree?.layout(height, width);
this.currentHeight = height;
this.currentWidth = width;
}
@ -421,12 +399,6 @@ export class CommentsPanel extends ViewPane implements ICommentsView {
}
}
public override getActionViewItem(action: IAction): IActionViewItem | undefined {
if (action.id === `workbench.actions.treeView.${this.id}.filter`) {
return this.instantiationService.createInstance(CommentsFilterActionViewItem, action, this);
}
return super.getActionViewItem(action);
}
}
CommandsRegistry.registerCommand({

View file

@ -3,50 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer } from 'vs/base/common/async';
import * as DOM from 'vs/base/browser/dom';
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { badgeBackground, badgeForeground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ContextScopedHistoryInputBox } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
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 { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { Codicon } from 'vs/base/common/codicons';
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint';
import { CommentsViewFilterFocusContextKey, CommentsViewSmallLayoutContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
import { 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';
const CONTEXT_KEY_SHOW_RESOLVED = new RawContextKey<boolean>('commentsView.showResolvedFilter', true);
const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey<boolean>('commentsView.showUnResolvedFilter', true);
export interface CommentsFiltersChangeEvent {
filterText?: boolean;
showResolved?: boolean;
showUnresolved?: boolean;
layout?: boolean;
}
export interface CommentsFiltersOptions {
filterText: string;
filterHistory: string[];
showResolved: boolean;
showUnresolved: boolean;
layout: DOM.Dimension;
}
export class CommentsFilters extends Disposable {
@ -54,323 +35,34 @@ export class CommentsFilters extends Disposable {
private readonly _onDidChange: Emitter<CommentsFiltersChangeEvent> = this._register(new Emitter<CommentsFiltersChangeEvent>());
readonly onDidChange: Event<CommentsFiltersChangeEvent> = this._onDidChange.event;
constructor(options: CommentsFiltersOptions) {
constructor(options: CommentsFiltersOptions, private readonly contextKeyService: IContextKeyService) {
super();
this._filterText = options.filterText;
this._showResolved = options.showResolved;
this._showUnresolved = options.showUnresolved;
this.filterHistory = options.filterHistory;
this._layout = options.layout;
this._showResolved.set(options.showResolved);
this._showUnresolved.set(options.showUnresolved);
}
private _filterText: string;
get filterText(): string {
return this._filterText;
}
set filterText(filterText: string) {
if (this._filterText !== filterText) {
this._filterText = filterText;
this._onDidChange.fire({ filterText: true });
}
}
filterHistory: string[];
private _showUnresolved: boolean = true;
private readonly _showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService);
get showUnresolved(): boolean {
return this._showUnresolved;
return !!this._showUnresolved.get();
}
set showUnresolved(showUnresolved: boolean) {
if (this._showUnresolved !== showUnresolved) {
this._showUnresolved = showUnresolved;
if (this._showUnresolved.get() !== showUnresolved) {
this._showUnresolved.set(showUnresolved);
this._onDidChange.fire(<CommentsFiltersChangeEvent>{ showUnresolved: true });
}
}
private _showResolved: boolean = true;
private _showResolved = CONTEXT_KEY_SHOW_RESOLVED.bindTo(this.contextKeyService);
get showResolved(): boolean {
return this._showResolved;
return !!this._showResolved.get();
}
set showResolved(showResolved: boolean) {
if (this._showResolved !== showResolved) {
this._showResolved = showResolved;
if (this._showResolved.get() !== showResolved) {
this._showResolved.set(showResolved);
this._onDidChange.fire(<CommentsFiltersChangeEvent>{ showResolved: true });
}
}
private _layout: DOM.Dimension = new DOM.Dimension(0, 0);
get layout(): DOM.Dimension {
return this._layout;
}
set layout(layout: DOM.Dimension) {
if (this._layout.width !== layout.width || this._layout.height !== layout.height) {
this._layout = layout;
this._onDidChange.fire(<CommentsFiltersChangeEvent>{ layout: true });
}
}
}
class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem {
constructor(
action: IAction, private filters: CommentsFilters, actionRunner: IActionRunner,
@IContextMenuService contextMenuService: IContextMenuService
) {
super(action,
{ getActions: () => this.getActions() },
contextMenuService,
{
actionRunner,
classNames: action.class,
anchorAlignmentProvider: () => AnchorAlignment.RIGHT,
menuAsChild: true
}
);
}
override render(container: HTMLElement): void {
super.render(container);
this.updateChecked();
}
private getActions(): IAction[] {
return [
{
checked: this.filters.showResolved,
class: undefined,
enabled: true,
id: 'showResolved',
label: localize('showResolved', "Show Resolved"),
run: async () => this.filters.showResolved = !this.filters.showResolved,
tooltip: ''
},
{
checked: this.filters.showUnresolved,
class: undefined,
enabled: true,
id: 'showUnresolved',
label: localize('showUnresolved', "Show Unresolved"),
run: async () => this.filters.showUnresolved = !this.filters.showUnresolved,
tooltip: ''
}
];
}
override updateChecked(): void {
this.element!.classList.toggle('checked', this._action.checked);
}
}
const filterIcon = registerIcon('comments-view-filter', Codicon.filter, localize('comments.filterIcon', 'Icon for the filter configuration in the Comments view.'));
export class CommentsFilterActionViewItem extends BaseActionViewItem {
private delayedFilterUpdate: Delayer<void>;
private container: HTMLElement | null = null;
private filterInputBox: HistoryInputBox | null = null;
private filterBadge: HTMLElement | null = null;
private focusContextKey: IContextKey<boolean>;
private readonly filtersAction: IAction;
private actionbar: ActionBar | null = null;
private keybindingService;
constructor(
action: IAction,
private commentsView: ICommentsView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService
) {
super(null, action);
this.keybindingService = keybindingService;
this.focusContextKey = CommentsViewFilterFocusContextKey.bindTo(contextKeyService);
this.delayedFilterUpdate = new Delayer<void>(400);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
this._register(commentsView.onDidFocusFilter(() => this.focus()));
this._register(commentsView.onDidClearFilterText(() => this.clearFilterText()));
this.filtersAction = new Action('commentsFiltersAction', localize('commentsFiltersAction', "More Filters..."), 'comments-filters ' + ThemeIcon.asClassName(filterIcon));
this.filtersAction.checked = this.hasFiltersChanged();
this._register(commentsView.filters.onDidChange(e => this.onDidFiltersChange(e)));
}
override render(container: HTMLElement): void {
this.container = container;
this.container.classList.add('comments-panel-action-filter-container');
this.element = DOM.append(this.container, DOM.$(''));
this.element.className = this.class;
this.createInput(this.element);
this.createControls(this.element);
this.updateClass();
this.adjustInputBox();
}
override focus(): void {
if (this.filterInputBox) {
this.filterInputBox.focus();
}
}
override blur(): void {
if (this.filterInputBox) {
this.filterInputBox.blur();
}
}
override setFocusable(): void {
// noop input elements are focusable by default
}
override get trapsArrowNavigation(): boolean {
return true;
}
private clearFilterText(): void {
if (this.filterInputBox) {
this.filterInputBox.value = '';
}
}
private onDidFiltersChange(e: CommentsFiltersChangeEvent): void {
this.filtersAction.checked = this.hasFiltersChanged();
if (e.layout) {
this.updateClass();
}
}
private hasFiltersChanged(): boolean {
return !this.commentsView.filters.showResolved || !this.commentsView.filters.showUnresolved;
}
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: localize('comments.filter.placeholder', "Filter (e.g. text, author)"),
ariaLabel: localize('comments.filter.ariaLabel', "Filter comments"),
history: this.commentsView.filters.filterHistory,
showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService)
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.commentsView.filters.filterText;
this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!))));
this._register(this.commentsView.filters.onDidChange((event: CommentsFiltersChangeEvent) => {
if (event.filterText) {
this.filterInputBox!.value = this.commentsView.filters.filterText;
}
}));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!)));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => {
e.stopPropagation();
e.preventDefault();
}));
const focusTracker = this._register(DOM.trackFocus(this.filterInputBox.inputElement));
this._register(focusTracker.onDidFocus(() => this.focusContextKey.set(true)));
this._register(focusTracker.onDidBlur(() => this.focusContextKey.set(false)));
this._register(toDisposable(() => this.focusContextKey.reset()));
}
private createControls(container: HTMLElement): void {
const controlsContainer = DOM.append(container, DOM.$('.comments-panel-filter-controls'));
this.createBadge(controlsContainer);
this.createFilters(controlsContainer);
}
private createBadge(container: HTMLElement): void {
const filterBadge = this.filterBadge = DOM.append(container, DOM.$('.comments-panel-filter-badge'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
filterBadge.style.backgroundColor = background;
filterBadge.style.borderWidth = border ? '1px' : '';
filterBadge.style.borderStyle = border ? 'solid' : '';
filterBadge.style.borderColor = border;
filterBadge.style.color = foreground;
}));
this.updateBadge();
this._register(this.commentsView.onDidChangeFilterStats(() => this.updateBadge()));
}
private createFilters(container: HTMLElement): void {
this.actionbar = this._register(new ActionBar(container, {
actionViewItemProvider: action => {
if (action.id === this.filtersAction.id) {
return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.commentsView.filters, this.actionRunner);
}
return undefined;
}
}));
this.actionbar.push(this.filtersAction, { icon: true, label: false });
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this.commentsView.filters.filterText = inputbox.value;
this.commentsView.filters.filterHistory = inputbox.getHistory();
}
private updateBadge(): void {
if (this.filterBadge) {
const { total, filtered } = this.commentsView.getFilterStats();
this.filterBadge.classList.toggle('hidden', (total === filtered && !this.filterInputBox?.value) || total === 0);
this.filterBadge.textContent = localize('showing filtered comments', "Showing {0} of {1}", filtered, total);
this.adjustInputBox();
}
}
private adjustInputBox(): void {
if (this.element && this.filterInputBox && this.filterBadge) {
this.filterInputBox.inputElement.style.paddingRight = this.element.classList.contains('small') || this.filterBadge.classList.contains('hidden') ? '25px' : '150px';
}
}
// Action toolbar is swallowing some keys for action items which should not be for an input box
private handleKeyboardEvent(event: StandardKeyboardEvent) {
if (event.equals(KeyCode.Space)
|| event.equals(KeyCode.LeftArrow)
|| event.equals(KeyCode.RightArrow)
) {
event.stopPropagation();
}
}
private onInputKeyDown(event: StandardKeyboardEvent, filterInputBox: HistoryInputBox) {
let handled = false;
if (event.equals(KeyCode.Tab)) {
this.actionbar?.focus();
handled = true;
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
protected override updateClass(): void {
if (this.element && this.container) {
this.element.className = this.class;
this.container.classList.toggle('grow', this.element.classList.contains('grow'));
this.adjustInputBox();
}
}
protected get class(): string {
if (this.commentsView.filters.layout.width > 600) {
return 'comments-panel-action-filter grow';
} else if (this.commentsView.filters.layout.width < 400) {
return 'comments-panel-action-filter small';
} else {
return 'comments-panel-action-filter';
}
}
}
registerAction2(class extends ViewAction<ICommentsView> {
@ -409,23 +101,6 @@ registerAction2(class extends ViewAction<ICommentsView> {
}
});
registerAction2(class extends Action2 {
constructor() {
super({
_isFakeAction: true,
id: `workbench.actions.treeView.${COMMENTS_VIEW_ID}.filter`,
title: localize('filter', "Filter"),
menu: {
id: MenuId.ViewTitle,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', COMMENTS_VIEW_ID), CommentsViewSmallLayoutContextKey.negate()),
group: 'navigation',
order: 1,
},
});
}
async run(): Promise<void> { }
});
registerAction2(class extends ViewAction<ICommentsView> {
constructor() {
super({
@ -444,17 +119,52 @@ registerAction2(class extends ViewAction<ICommentsView> {
}
});
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const inputActiveOptionBorderColor = theme.getColor(inputActiveOptionBorder);
if (inputActiveOptionBorderColor) {
collector.addRule(`.comments-panel-action-filter > .comments-panel-filter-controls > .monaco-action-bar .action-label.comments-filters.checked { border-color: ${inputActiveOptionBorderColor}; }`);
registerAction2(class extends ViewAction<ICommentsView> {
constructor() {
super({
id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`,
title: localize('toggle unresolved', "Toggle Unresolved Comments"),
category: localize('comments', "Comments"),
toggled: {
condition: CONTEXT_KEY_SHOW_UNRESOLVED,
title: localize('unresolved', "Show Unresolved"),
},
menu: {
id: viewFilterSubmenu,
group: '1_filter',
when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID),
order: 1
},
viewId: COMMENTS_VIEW_ID
});
}
const inputActiveOptionForegroundColor = theme.getColor(inputActiveOptionForeground);
if (inputActiveOptionForegroundColor) {
collector.addRule(`.comments-panel-action-filter > .comments-panel-filter-controls > .monaco-action-bar .action-label.comments-filters.checked { color: ${inputActiveOptionForegroundColor}; }`);
}
const inputActiveOptionBackgroundColor = theme.getColor(inputActiveOptionBackground);
if (inputActiveOptionBackgroundColor) {
collector.addRule(`.comments-panel-action-filter > .comments-panel-filter-controls > .monaco-action-bar .action-label.comments-filters.checked { background-color: ${inputActiveOptionBackgroundColor}; }`);
async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise<void> {
view.filters.showUnresolved = !view.filters.showUnresolved;
}
});
registerAction2(class extends ViewAction<ICommentsView> {
constructor() {
super({
id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`,
title: localize('toggle resolved', "Toggle Resolved Comments"),
category: localize('comments', "Comments"),
toggled: {
condition: CONTEXT_KEY_SHOW_RESOLVED,
title: localize('resolved', "Show Resolved"),
},
menu: {
id: viewFilterSubmenu,
group: '1_filter',
when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID),
order: 1
},
viewId: COMMENTS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise<void> {
view.filters.showResolved = !view.filters.showResolved;
}
});

View file

@ -109,61 +109,6 @@
padding-left: 16px;
}
.comments-panel-container .monaco-action-bar.comments-panel-filter-container .action-item.comments-panel-action-filter-container,
.panel > .title .monaco-action-bar .action-item.comments-panel-action-filter-container.grow {
flex: 1;
}
.comments-panel-container .monaco-action-bar.comments-panel-filter-container {
margin: 10px 20px;
height: initial;
}
.comments-panel-action-filter > .comments-panel-filter-controls {
position: absolute;
top: 0px;
bottom: 0;
right: 0px;
display: flex;
align-items: center;
}
.comments-panel-action-filter > .comments-panel-filter-controls > .comments-panel-filter-badge {
margin: 4px 0px;
padding: 0px 8px;
border-radius: 2px;
}
.comments-panel-action-filter > .comments-panel-filter-controls > .comments-panel-filter-badge.hidden,
.comments-panel-action-filter.small > .comments-panel-filter-controls > .comments-panel-filter-badge {
display: none;
}
.comments-panel-action-filter > .comments-panel-filter-controls > .monaco-action-bar .action-item .action-label.codicon.comments-filters {
padding: 2px;
}
.panel > .title .monaco-action-bar .action-item.comments-panel-action-filter-container {
max-width: 400px;
min-width: 300px;
margin-right: 10px;
}
.monaco-action-bar .comments-panel-action-filter .monaco-inputbox {
height: 24px;
font-size: 12px;
flex: 1;
}
.pane-header .monaco-action-bar .comments-panel-action-filter .monaco-inputbox {
height: 20px;
line-height: 18px;
}
.monaco-workbench.vs .monaco-action-bar .comments-panel-action-filter .monaco-inputbox {
height: 25px;
}
.comments-panel .hide {
display: none;
}

View file

@ -142,53 +142,3 @@
}
.monaco-workbench .repl .repl-tree .output.expression .code-subscript { vertical-align: sub; font-size: smaller; line-height: normal; }
.monaco-workbench .repl .repl-tree .output.expression .code-superscript { vertical-align: super; font-size: smaller; line-height: normal; }
.monaco-action-bar .action-item.repl-panel-filter-container {
cursor: default;
display: flex;
}
.monaco-action-bar .panel-action-tree-filter{
display: flex;
align-items: center;
flex: 1;
}
.monaco-action-bar .panel-action-tree-filter .monaco-inputbox {
height: 24px;
font-size: 12px;
flex: 1;
}
.pane-header .monaco-action-bar .panel-action-tree-filter .monaco-inputbox {
height: 20px;
line-height: 18px;
}
.monaco-workbench.vs .monaco-action-bar .panel-action-tree-filter .monaco-inputbox {
height: 25px;
}
.panel > .title .monaco-action-bar .action-item.repl-panel-filter-container {
min-width: 300px;
margin-right: 10px;
}
.repl-panel-filter-container .repl-panel-filter-controls {
position: absolute;
top: 0px;
bottom: 0;
right: 0px;
display: flex;
align-items: center;
}
.repl-panel-filter-container .repl-panel-filter-controls > .repl-panel-filter-badge {
margin: 4px;
padding: 0px 8px;
border-radius: 2px;
}
.repl-panel-filter-container .repl-panel-filter-controls > .repl-panel-filter-badge.hidden {
display: none;
}

View file

@ -44,7 +44,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@ -55,13 +54,13 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { FilterViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views';
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems';
import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from 'vs/workbench/contrib/debug/browser/debugIcons';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { ReplFilter, ReplFilterActionViewItem, ReplFilterState } from 'vs/workbench/contrib/debug/browser/replFilter';
import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter';
import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplRawObjectsRenderer, ReplSimpleElementsRenderer, ReplVariablesRenderer } from 'vs/workbench/contrib/debug/browser/replViewer';
import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, getStateLabel, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State } from 'vs/workbench/contrib/debug/common/debug';
import { Variable } from 'vs/workbench/contrib/debug/common/debugModel';
@ -74,7 +73,6 @@ const HISTORY_STORAGE_KEY = 'debug.repl.history';
const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory';
const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue';
const DECORATION_KEY = 'replinputdecoration';
const FILTER_ACTION_ID = `workbench.actions.treeView.repl.filter`;
function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {
tree.scrollTop = tree.scrollHeight - tree.renderHeight;
@ -84,7 +82,7 @@ function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {
const sessionsToIgnore = new Set<IDebugSession>();
const identityProvider = { getId: (element: IReplElement) => element.getId() };
export class Repl extends ViewPane implements IHistoryNavigationWidget {
export class Repl extends FilterViewPane implements IHistoryNavigationWidget {
declare readonly _serviceBrand: undefined;
private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show
@ -99,7 +97,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
private treeContainer!: HTMLElement;
private replInput!: CodeEditorWidget;
private replInputContainer!: HTMLElement;
private dimension!: dom.Dimension;
private bodyContentDimension: dom.Dimension | undefined;
private replInputLineCount = 1;
private model: ITextModel | undefined;
private setHistoryNavigationEnablement!: (enabled: boolean) => void;
@ -109,8 +107,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
private completionItemProvider: IDisposable | undefined;
private modelChangeListener: IDisposable = Disposable.None;
private filter: ReplFilter;
private filterState: ReplFilterState;
private filterActionViewItem: ReplFilterActionViewItem | undefined;
private multiSessionRepl: IContextKey<boolean>;
private menu: IMenu;
@ -134,14 +130,21 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
@IMenuService menuService: IMenuService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
const filterText = storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, '');
super({
...options,
filterOptions: {
placeholder: localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude)"),
text: filterText,
history: JSON.parse(storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[],
}
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService);
this._register(this.menu);
this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50);
this.filter = new ReplFilter();
this.filterState = new ReplFilterState(this);
this.filter.filterQuery = this.filterState.filterText = this.storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, '');
this.filter.filterQuery = filterText;
this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService);
this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getBackgroundColor()));
this._register(this.replOptions.onDidChange(() => this.onDidStyleChange()));
@ -186,7 +189,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.replInput.setModel(this.model);
this.updateInputDecoration();
this.refreshReplElements(true);
this.layoutBody(this.dimension.height, this.dimension.width);
}
}));
this._register(this.configurationService.onDidChangeConfiguration(e => {
@ -208,8 +210,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.setMode();
}));
this._register(this.filterState.onDidChange(() => {
this.filter.filterQuery = this.filterState.filterText;
this._register(this.filterWidget.onDidChangeFilterText(() => {
this.filter.filterQuery = this.filterWidget.getFilterText();
this.tree.refilter();
revealLastElement(this.tree);
}));
@ -318,7 +320,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
}
focusFilter(): void {
this.filterActionViewItem?.focus();
this.filterWidget.focus();
}
private setMode(): void {
@ -364,8 +366,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.tree.rerender();
if (this.dimension) {
this.layoutBody(this.dimension.height, this.dimension.width);
if (this.bodyContentDimension) {
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
}
}
}
@ -431,9 +433,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.replInput.setValue('');
const shouldRelayout = this.replInputLineCount > 1;
this.replInputLineCount = 1;
if (shouldRelayout) {
if (shouldRelayout && this.bodyContentDimension) {
// Trigger a layout to shrink a potential multi line input
this.layoutBody(this.dimension.height, this.dimension.width);
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
}
}
}
@ -458,9 +460,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
return removeAnsiEscapeCodes(text);
}
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
this.dimension = new dom.Dimension(width, height);
protected override layoutBodyContent(height: number, width: number): void {
this.bodyContentDimension = new dom.Dimension(width, height);
const replInputHeight = Math.min(this.replInput.getContentHeight(), height);
if (this.tree) {
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;
@ -476,6 +477,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.replInput.layout({ width: width - 30, height: replInputHeight });
}
override shouldShowFilterInHeader(): boolean {
return true;
}
collapseAll(): void {
this.tree.collapseAll();
}
@ -492,11 +497,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
if (action.id === selectReplCommandId) {
const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession;
return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);
} else if (action.id === FILTER_ACTION_ID) {
const filterHistory = JSON.parse(this.storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[];
this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action,
localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude)"), this.filterState, filterHistory, () => showHistoryKeybindingHint(this.keybindingService));
return this.filterActionViewItem;
}
return super.getActionViewItem(action);
@ -538,7 +538,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
await autoExpandElements(session.getReplElements());
}
// Repl elements count changed, need to update filter stats on the badge
this.filterState.updateFilterStats();
const { total, filtered } = this.getFilterStats();
this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : localize('showing filtered repl lines', "Showing {0} of {1}", filtered, total));
}, Repl.REFRESH_DELAY);
}
@ -645,7 +646,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
const lineCount = model ? Math.min(10, model.getLineCount()) : 1;
if (lineCount !== this.replInputLineCount) {
this.replInputLineCount = lineCount;
this.layoutBody(this.dimension.height, this.dimension.width);
if (this.bodyContentDimension) {
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
}
}
}));
// We add the input decoration only when the focus is in the input #61126
@ -712,19 +715,17 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
} else {
this.storageService.remove(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
}
if (this.filterActionViewItem) {
const filterHistory = this.filterActionViewItem.getHistory();
if (filterHistory.length) {
this.storageService.store(FILTER_HISTORY_STORAGE_KEY, JSON.stringify(filterHistory), StorageScope.WORKSPACE, StorageTarget.USER);
} else {
this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
}
const filterValue = this.filterState.filterText;
if (filterValue) {
this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.USER);
} else {
this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE);
}
const filterHistory = this.filterWidget.getHistory();
if (filterHistory.length) {
this.storageService.store(FILTER_HISTORY_STORAGE_KEY, JSON.stringify(filterHistory), StorageScope.WORKSPACE, StorageTarget.USER);
} else {
this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
}
const filterValue = this.filterWidget.getFilterText();
if (filterValue) {
this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.USER);
} else {
this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE);
}
super.saveState();
@ -876,27 +877,6 @@ function getReplView(viewsService: IViewsService): Repl | undefined {
return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined;
}
registerAction2(class extends Action2 {
constructor() {
super({
_isFakeAction: true,
id: FILTER_ACTION_ID,
title: localize('filter', "Filter"),
f1: false,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', REPL_VIEW_ID),
order: 10
},
});
}
run(_accessor: ServicesAccessor) {
// noop this action is just a placeholder for the filter action view item
}
});
const selectReplCommandId = 'workbench.action.debug.selectRepl';
registerAction2(class extends ViewAction<Repl> {
constructor() {

View file

@ -7,23 +7,7 @@ import { matchesFuzzy } from 'vs/base/common/filters';
import { splitGlobAware } from 'vs/base/common/glob';
import { ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
import { IReplElement } from 'vs/workbench/contrib/debug/common/debug';
import * as DOM from 'vs/base/browser/dom';
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { Delayer } from 'vs/base/common/async';
import { IAction } from 'vs/base/common/actions';
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { toDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ContextScopedHistoryInputBox } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { ReplEvaluationResult, ReplEvaluationInput } from 'vs/workbench/contrib/debug/common/replModel';
import { localize } from 'vs/nls';
import { Variable } from 'vs/workbench/contrib/debug/common/debugModel';
@ -79,184 +63,3 @@ export class ReplFilter implements ITreeFilter<IReplElement> {
return includeQueryPresent ? includeQueryMatched : (typeof parentVisibility !== 'undefined' ? parentVisibility : TreeVisibility.Visible);
}
}
export interface IFilterStatsProvider {
getFilterStats(): { total: number; filtered: number };
}
export class ReplFilterState {
constructor(private filterStatsProvider: IFilterStatsProvider) { }
private readonly _onDidChange: Emitter<void> = new Emitter<void>();
get onDidChange(): Event<void> {
return this._onDidChange.event;
}
private readonly _onDidStatsChange: Emitter<void> = new Emitter<void>();
get onDidStatsChange(): Event<void> {
return this._onDidStatsChange.event;
}
private _filterText = '';
private _stats = { total: 0, filtered: 0 };
get filterText(): string {
return this._filterText;
}
get filterStats(): { total: number; filtered: number } {
return this._stats;
}
set filterText(filterText: string) {
if (this._filterText !== filterText) {
this._filterText = filterText;
this._onDidChange.fire();
this.updateFilterStats();
}
}
updateFilterStats(): void {
const { total, filtered } = this.filterStatsProvider.getFilterStats();
if (this._stats.total !== total || this._stats.filtered !== filtered) {
this._stats = { total, filtered };
this._onDidStatsChange.fire();
}
}
}
export class ReplFilterActionViewItem extends BaseActionViewItem {
private delayedFilterUpdate: Delayer<void>;
private container!: HTMLElement;
private filterBadge!: HTMLElement;
private filterInputBox!: HistoryInputBox;
constructor(
action: IAction,
private placeholder: string,
private filters: ReplFilterState,
private history: string[],
private showHistoryHint: () => boolean,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService) {
super(null, action);
this.delayedFilterUpdate = new Delayer<void>(400);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
}
override render(container: HTMLElement): void {
this.container = container;
this.container.classList.add('repl-panel-filter-container');
this.element = DOM.append(this.container, DOM.$(''));
this.element.className = this.class;
this.createInput(this.element);
this.createBadge(this.element);
this.updateClass();
}
override focus(): void {
this.filterInputBox?.focus();
}
override blur(): void {
this.filterInputBox?.blur();
}
override setFocusable(): void {
// noop input elements are focusable by default
}
getHistory(): string[] {
return this.filterInputBox.getHistory();
}
override get trapsArrowNavigation(): boolean {
return true;
}
private clearFilterText(): void {
this.filterInputBox.value = '';
}
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: this.placeholder,
history: this.history,
showHistoryHint: this.showHistoryHint
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.filters.filterText;
this._register(this.filterInputBox.onDidChange(() => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!))));
this._register(this.filters.onDidChange(() => {
this.filterInputBox.value = this.filters.filterText;
}));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e)));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => {
e.stopPropagation();
e.preventDefault();
}));
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this.filters.filterText = inputbox.value;
}
// Action toolbar is swallowing some keys for action items which should not be for an input box
private handleKeyboardEvent(event: StandardKeyboardEvent) {
if (event.equals(KeyCode.Space)
|| event.equals(KeyCode.LeftArrow)
|| event.equals(KeyCode.RightArrow)
|| event.equals(KeyCode.Escape)
) {
event.stopPropagation();
}
}
private onInputKeyDown(event: StandardKeyboardEvent) {
if (event.equals(KeyCode.Escape)) {
this.clearFilterText();
event.stopPropagation();
event.preventDefault();
}
}
private createBadge(container: HTMLElement): void {
const controlsContainer = DOM.append(container, DOM.$('.repl-panel-filter-controls'));
const filterBadge = this.filterBadge = DOM.append(controlsContainer, DOM.$('.repl-panel-filter-badge'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
filterBadge.style.backgroundColor = background;
filterBadge.style.borderWidth = border ? '1px' : '';
filterBadge.style.borderStyle = border ? 'solid' : '';
filterBadge.style.borderColor = border;
filterBadge.style.color = foreground;
}));
this.updateBadge();
this._register(this.filters.onDidStatsChange(() => this.updateBadge()));
}
private updateBadge(): void {
const { total, filtered } = this.filters.filterStats;
const filterBadgeHidden = total === filtered || total === 0;
this.filterBadge.classList.toggle('hidden', filterBadgeHidden);
this.filterBadge.textContent = localize('showing filtered repl lines', "Showing {0} of {1}", filtered, total);
this.filterInputBox.inputElement.style.paddingRight = filterBadgeHidden ? '4px' : '150px';
}
protected get class(): string {
return 'panel-action-tree-filter';
}
}

View file

@ -32,6 +32,7 @@ import { Codicon } from 'vs/base/common/codicons';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { viewFilterSubmenu } from 'vs/workbench/browser/parts/views/viewFilter';
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Markers.MARKER_OPEN_ACTION_ID,
@ -196,6 +197,131 @@ registerAction2(class extends ViewAction<IMarkersView> {
}
});
registerAction2(class extends ViewAction<IMarkersView> {
constructor() {
super({
id: `workbench.actions.${Markers.MARKERS_VIEW_ID}.toggleErrors`,
title: localize('toggle errors', "Toggle Errors"),
category: localize('problems', "Problems"),
toggled: {
condition: MarkersContextKeys.ShowErrorsFilterContextKey,
title: localize('errors', "Show Errors")
},
menu: {
id: viewFilterSubmenu,
group: '1_filter',
when: ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID),
order: 1
},
viewId: Markers.MARKERS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> {
view.filters.showErrors = !view.filters.showErrors;
}
});
registerAction2(class extends ViewAction<IMarkersView> {
constructor() {
super({
id: `workbench.actions.${Markers.MARKERS_VIEW_ID}.toggleWarnings`,
title: localize('toggle warnings', "Toggle Warnings"),
category: localize('problems', "Problems"),
toggled: {
condition: MarkersContextKeys.ShowWarningsFilterContextKey,
title: localize('warnings', "Show Warnings")
},
menu: {
id: viewFilterSubmenu,
group: '1_filter',
when: ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID),
order: 2
},
viewId: Markers.MARKERS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> {
view.filters.showWarnings = !view.filters.showWarnings;
}
});
registerAction2(class extends ViewAction<IMarkersView> {
constructor() {
super({
id: `workbench.actions.${Markers.MARKERS_VIEW_ID}.toggleInfos`,
title: localize('toggle infos', "Toggle Infos"),
category: localize('problems', "Problems"),
toggled: {
condition: MarkersContextKeys.ShowInfoFilterContextKey,
title: localize('Infos', "Show Infos")
},
menu: {
id: viewFilterSubmenu,
group: '1_filter',
when: ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID),
order: 3
},
viewId: Markers.MARKERS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> {
view.filters.showInfos = !view.filters.showInfos;
}
});
registerAction2(class extends ViewAction<IMarkersView> {
constructor() {
super({
id: `workbench.actions.${Markers.MARKERS_VIEW_ID}.toggleActiveFile`,
title: localize('toggle active file', "Toggle Active File"),
category: localize('problems', "Problems"),
toggled: {
condition: MarkersContextKeys.ShowActiveFileFilterContextKey,
title: localize('Active File', "Show Active File Only")
},
menu: {
id: viewFilterSubmenu,
group: '2_filter',
when: ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID),
order: 1
},
viewId: Markers.MARKERS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> {
view.filters.activeFile = !view.filters.activeFile;
}
});
registerAction2(class extends ViewAction<IMarkersView> {
constructor() {
super({
id: `workbench.actions.${Markers.MARKERS_VIEW_ID}.toggleExcludedFiles`,
title: localize('toggle Excluded Files', "Toggle Excluded Files"),
category: localize('problems', "Problems"),
toggled: {
condition: MarkersContextKeys.ShowExcludedFilesFilterContextKey,
title: localize('Excluded Files', "Hide Excluded Files")
},
menu: {
id: viewFilterSubmenu,
group: '2_filter',
when: ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID),
order: 2
},
viewId: Markers.MARKERS_VIEW_ID
});
}
async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> {
view.filters.excludedFiles = !view.filters.excludedFiles;
}
});
registerAction2(class extends Action2 {
constructor() {
super({
@ -406,23 +532,6 @@ registerAction2(class extends ViewAction<IMarkersView> {
}
});
registerAction2(class extends Action2 {
constructor() {
super({
_isFakeAction: true,
id: `workbench.actions.treeView.${Markers.MARKERS_VIEW_ID}.filter`,
title: localize('filter', "Filter"),
menu: {
id: MenuId.ViewTitle,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersViewSmallLayoutContextKey.negate()),
group: 'navigation',
order: 1,
},
});
}
async run(): Promise<void> { }
});
registerAction2(class extends Action2 {
constructor() {
super({

View file

@ -4,17 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { MarkersFilters } from 'vs/workbench/contrib/markers/browser/markersViewActions';
import { Event } from 'vs/base/common/event';
import { IView } from 'vs/workbench/common/views';
import { MarkerElement, ResourceMarkers } from 'vs/workbench/contrib/markers/browser/markersModel';
import { MarkersViewMode } from 'vs/workbench/contrib/markers/common/markers';
export interface IMarkersView extends IView {
readonly onDidFocusFilter: Event<void>;
readonly onDidClearFilterText: Event<void>;
readonly filters: MarkersFilters;
readonly onDidChangeFilterStats: Event<{ total: number; filtered: number }>;
focusFilter(): void;
clearFilterText(): void;
getFilterStats(): { total: number; filtered: number };

View file

@ -7,12 +7,12 @@ import 'vs/css!./media/markers';
import { URI } from 'vs/base/common/uri';
import * as dom from 'vs/base/browser/dom';
import { IAction, Action, Separator } from 'vs/base/common/actions';
import { IAction, Separator } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement, MarkerTableItem } from 'vs/workbench/contrib/markers/browser/markersModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions';
import { MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import Messages from 'vs/workbench/contrib/markers/browser/messages';
import { RangeHighlightDecorations } from 'vs/workbench/browser/codeeditor';
@ -23,7 +23,7 @@ import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Iterable } from 'vs/base/common/iterator';
import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer, ITreeEvent } from 'vs/base/browser/ui/tree/tree';
import { Relay, Event, Emitter } from 'vs/base/common/event';
import { Relay, Event } from 'vs/base/common/event';
import { WorkbenchObjectTree, IListService, IWorkbenchObjectTreeOptions, IOpenEvent } from 'vs/platform/list/browser/listService';
import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions';
import { IExpression } from 'vs/base/common/glob';
@ -31,7 +31,6 @@ import { deepClone } from 'vs/base/common/objects';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersWidgetAccessibilityProvider, MarkersViewModel } from 'vs/workbench/contrib/markers/browser/markersTreeViewer';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
@ -42,7 +41,7 @@ import { MementoObject, Memento } from 'vs/workbench/common/memento';
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { KeyCode } from 'vs/base/common/keyCodes';
import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewPaneOptions, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService, withSelection } from 'vs/platform/opener/common/opener';
import { Codicon } from 'vs/base/common/codicons';
@ -94,7 +93,7 @@ export interface IProblemsWidget {
updateMarker(marker: Marker): void;
}
export class MarkersView extends ViewPane implements IMarkersView {
export class MarkersView extends FilterViewPane implements IMarkersView {
private lastSelectedRelativeTop: number = 0;
private currentActiveResource: URI | null = null;
@ -109,7 +108,6 @@ export class MarkersView extends ViewPane implements IMarkersView {
private widgetContainer!: HTMLElement;
private widgetIdentityProvider: IIdentityProvider<MarkerElement | MarkerTableItem>;
private widgetAccessibilityProvider: MarkersWidgetAccessibilityProvider;
private filterActionBar: ActionBar | undefined;
private messageBoxContainer: HTMLElement | undefined;
private ariaLabelElement: HTMLElement | undefined;
readonly filters: MarkersFilters;
@ -118,24 +116,13 @@ export class MarkersView extends ViewPane implements IMarkersView {
private currentWidth = 0;
private readonly panelState: MementoObject;
private _onDidChangeFilterStats = this._register(new Emitter<{ total: number; filtered: number }>());
readonly onDidChangeFilterStats: Event<{ total: number; filtered: number }> = this._onDidChangeFilterStats.event;
private cachedFilterStats: { total: number; filtered: number } | undefined = undefined;
private currentResourceGotAddedToMarkersData: boolean = false;
private readonly markersViewModel: MarkersViewModel;
private readonly smallLayoutContextKey: IContextKey<boolean>;
private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); }
private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); }
readonly onDidChangeVisibility = this.onDidChangeBodyVisibility;
private readonly _onDidFocusFilter: Emitter<void> = this._register(new Emitter<void>());
readonly onDidFocusFilter: Event<void> = this._onDidFocusFilter.event;
private readonly _onDidClearFilterText: Emitter<void> = this._register(new Emitter<void>());
readonly onDidClearFilterText: Event<void> = this._onDidClearFilterText.event;
constructor(
options: IViewPaneOptions,
@IInstantiationService instantiationService: IInstantiationService,
@ -153,9 +140,18 @@ export class MarkersView extends ViewPane implements IMarkersView {
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.smallLayoutContextKey = MarkersContextKeys.MarkersViewSmallLayoutContextKey.bindTo(this.contextKeyService);
this.panelState = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
const panelState = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
super({
...options,
filterOptions: {
ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL,
placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER,
focusContextKey: MarkersContextKeys.MarkerViewFilterFocusContextKey.key,
text: panelState['filter'] || '',
history: panelState['filterHistory'] || []
}
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.panelState = panelState;
this.markersModel = this._register(instantiationService.createInstance(MarkersModel));
this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'], this.panelState['viewMode'] ?? this.getDefaultViewMode()));
@ -171,15 +167,13 @@ export class MarkersView extends ViewPane implements IMarkersView {
this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations));
this.filters = this._register(new MarkersFilters({
filterText: this.panelState['filter'] || '',
filterHistory: this.panelState['filterHistory'] || [],
showErrors: this.panelState['showErrors'] !== false,
showWarnings: this.panelState['showWarnings'] !== false,
showInfos: this.panelState['showInfos'] !== false,
excludedFiles: !!this.panelState['useFilesExclude'],
activeFile: !!this.panelState['activeFile'],
layout: new dom.Dimension(0, 0)
}));
}, this.contextKeyService));
// Update filter, whenever the "files.exclude" setting is changed
this._register(this.configurationService.onDidChangeConfiguration(e => {
@ -203,9 +197,6 @@ export class MarkersView extends ViewPane implements IMarkersView {
this.createArialLabelElement(panelContainer);
this.createFilterActionBar(panelContainer);
this.filterActionBar!.push(new Action(`workbench.actions.treeView.${this.id}.filter`));
this.createMessageBox(panelContainer);
this.widgetContainer = dom.append(panelContainer, dom.$('.widget-container'));
@ -219,19 +210,11 @@ export class MarkersView extends ViewPane implements IMarkersView {
return Messages.MARKERS_PANEL_TITLE_PROBLEMS;
}
public override layoutBody(height: number = this.currentHeight, width: number = this.currentWidth): void {
super.layoutBody(height, width);
const wasSmallLayout = this.smallLayout;
this.smallLayout = width < 600 && height > 100;
if (this.smallLayout !== wasSmallLayout) {
this.filterActionBar?.getContainer().classList.toggle('hide', !this.smallLayout);
}
const contentHeight = this.smallLayout ? height - 44 : height;
public override layoutBodyContent(height: number = this.currentHeight, width: number = this.currentWidth): void {
if (this.messageBoxContainer) {
this.messageBoxContainer.style.height = `${contentHeight}px`;
this.messageBoxContainer.style.height = `${height}px`;
}
this.widget.layout(contentHeight, width);
this.filters.layout = new dom.Dimension(this.smallLayout ? width : width - 200, height);
this.widget.layout(height, width);
this.currentHeight = height;
this.currentWidth = width;
@ -251,11 +234,19 @@ export class MarkersView extends ViewPane implements IMarkersView {
}
public focusFilter(): void {
this._onDidFocusFilter.fire();
this.filterWidget.focus();
}
public updateBadge(total: number, filtered: number): void {
this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : localize('showing filtered problems', "Showing {0} of {1}", filtered, total));
}
public checkMoreFilters(): void {
this.filterWidget.checkMoreFilters(!this.filters.showErrors || !this.filters.showWarnings || !this.filters.showInfos || this.filters.excludedFiles || this.filters.activeFile);
}
public clearFilterText(): void {
this._onDidClearFilterText.fire();
this.filterWidget.setFilterText('');
}
public showQuickFixes(marker: Marker): void {
@ -323,7 +314,8 @@ export class MarkersView extends ViewPane implements IMarkersView {
this.toggleVisibility(total === 0 || filtered === 0);
this.renderMessage();
this._onDidChangeFilterStats.fire(this.getFilterStats());
this.updateBadge(total, filtered);
this.checkMoreFilters();
}
}
@ -336,7 +328,7 @@ export class MarkersView extends ViewPane implements IMarkersView {
}
private updateFilter() {
this.filter.options = new FilterOptions(this.filters.filterText, this.getFilesExcludeExpressions(), this.filters.showWarnings, this.filters.showErrors, this.filters.showInfos, this.uriIdentityService);
this.filter.options = new FilterOptions(this.filterWidget.getFilterText(), this.getFilesExcludeExpressions(), this.filters.showWarnings, this.filters.showErrors, this.filters.showInfos, this.uriIdentityService);
this.widget.filterMarkers(this.getResourceMarkers(), this.filter.options);
this.cachedFilterStats = undefined;
@ -344,7 +336,8 @@ export class MarkersView extends ViewPane implements IMarkersView {
this.toggleVisibility(total === 0 || filtered === 0);
this.renderMessage();
this._onDidChangeFilterStats.fire(this.getFilterStats());
this.updateBadge(total, filtered);
this.checkMoreFilters();
}
private getDefaultViewMode(): MarkersViewMode {
@ -389,12 +382,6 @@ export class MarkersView extends ViewPane implements IMarkersView {
return resourceMarkers;
}
private createFilterActionBar(parent: HTMLElement): void {
this.filterActionBar = this._register(new ActionBar(parent, { actionViewItemProvider: action => this.getActionViewItem(action) }));
this.filterActionBar.getContainer().classList.add('markers-panel-filter-container');
this.filterActionBar.getContainer().classList.toggle('hide', !this.smallLayout);
}
private createMessageBox(parent: HTMLElement): void {
this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container'));
this.messageBoxContainer.setAttribute('aria-labelledby', 'markers-panel-arialabel');
@ -558,10 +545,11 @@ export class MarkersView extends ViewPane implements IMarkersView {
disposables.push(this.filters.onDidChange((event: IMarkersFiltersChangeEvent) => {
if (event.activeFile) {
this.refreshPanel();
} else if (event.filterText || event.excludedFiles || event.showWarnings || event.showErrors || event.showInfos) {
} else if (event.excludedFiles || event.showWarnings || event.showErrors || event.showInfos) {
this.updateFilter();
}
}));
disposables.push(this.filterWidget.onDidChangeFilterText(e => this.updateFilter()));
disposables.push(toDisposable(() => { this.cachedFilterStats = undefined; }));
disposables.push(toDisposable(() => this.rangeHighlightDecorations.removeHighlightRange()));
@ -744,7 +732,7 @@ export class MarkersView extends ViewPane implements IMarkersView {
}
private clearFilters(): void {
this.filters.filterText = '';
this.filterWidget.setFilterText('');
this.filters.excludedFiles = false;
this.filters.showErrors = true;
this.filters.showWarnings = true;
@ -862,13 +850,6 @@ export class MarkersView extends ViewPane implements IMarkersView {
return this.markersModel.resourceMarkers;
}
public override getActionViewItem(action: IAction): IActionViewItem | undefined {
if (action.id === `workbench.actions.treeView.${this.id}.filter`) {
return this.instantiationService.createInstance(MarkersFilterActionViewItem, action, this);
}
return super.getActionViewItem(action);
}
getFilterStats(): { total: number; filtered: number } {
if (!this.cachedFilterStats) {
this.cachedFilterStats = {
@ -882,11 +863,11 @@ export class MarkersView extends ViewPane implements IMarkersView {
private toggleVisibility(hide: boolean): void {
this.widget.toggleVisibility(hide);
this.layoutBody();
this.layoutBodyContent();
}
override saveState(): void {
this.panelState['filter'] = this.filters.filterText;
this.panelState['filter'] = this.filterWidget.getFilterText();
this.panelState['filterHistory'] = this.filters.filterHistory;
this.panelState['showErrors'] = this.filters.showErrors;
this.panelState['showWarnings'] = this.filters.showWarnings;

View file

@ -3,54 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer } from 'vs/base/common/async';
import * as DOM from 'vs/base/browser/dom';
import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions';
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Action, IAction } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import Messages from 'vs/workbench/contrib/markers/browser/messages';
import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { badgeBackground, badgeForeground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ContextScopedHistoryInputBox } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService';
import { Disposable } from 'vs/base/common/lifecycle';
import { inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Event, Emitter } from 'vs/base/common/event';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { Codicon } from 'vs/base/common/codicons';
import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { MarkersContextKeys } from 'vs/workbench/contrib/markers/common/markers';
export interface IMarkersFiltersChangeEvent {
filterText?: boolean;
excludedFiles?: boolean;
showWarnings?: boolean;
showErrors?: boolean;
showInfos?: boolean;
activeFile?: boolean;
layout?: boolean;
}
export interface IMarkersFiltersOptions {
filterText: string;
filterHistory: string[];
showErrors: boolean;
showWarnings: boolean;
showInfos: boolean;
excludedFiles: boolean;
activeFile: boolean;
layout: DOM.Dimension;
}
export class MarkersFilters extends Disposable {
@ -58,384 +39,74 @@ export class MarkersFilters extends Disposable {
private readonly _onDidChange: Emitter<IMarkersFiltersChangeEvent> = this._register(new Emitter<IMarkersFiltersChangeEvent>());
readonly onDidChange: Event<IMarkersFiltersChangeEvent> = this._onDidChange.event;
constructor(options: IMarkersFiltersOptions) {
constructor(options: IMarkersFiltersOptions, private readonly contextKeyService: IContextKeyService) {
super();
this._filterText = options.filterText;
this._showErrors = options.showErrors;
this._showWarnings = options.showWarnings;
this._showInfos = options.showInfos;
this._excludedFiles = options.excludedFiles;
this._activeFile = options.activeFile;
this.filterHistory = options.filterHistory;
this._layout = options.layout;
}
private _filterText: string;
get filterText(): string {
return this._filterText;
}
set filterText(filterText: string) {
if (this._filterText !== filterText) {
this._filterText = filterText;
this._onDidChange.fire({ filterText: true });
}
this._showErrors.set(options.showErrors);
this._showWarnings.set(options.showWarnings);
this._showInfos.set(options.showInfos);
this._excludedFiles.set(options.excludedFiles);
this._activeFile.set(options.activeFile);
this.filterHistory = options.filterHistory;
}
filterHistory: string[];
private _excludedFiles: boolean;
private readonly _excludedFiles = MarkersContextKeys.ShowExcludedFilesFilterContextKey.bindTo(this.contextKeyService);
get excludedFiles(): boolean {
return this._excludedFiles;
return !!this._excludedFiles.get();
}
set excludedFiles(filesExclude: boolean) {
if (this._excludedFiles !== filesExclude) {
this._excludedFiles = filesExclude;
if (this._excludedFiles.get() !== filesExclude) {
this._excludedFiles.set(filesExclude);
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ excludedFiles: true });
}
}
private _activeFile: boolean;
private readonly _activeFile = MarkersContextKeys.ShowActiveFileFilterContextKey.bindTo(this.contextKeyService);
get activeFile(): boolean {
return this._activeFile;
return !!this._activeFile.get();
}
set activeFile(activeFile: boolean) {
if (this._activeFile !== activeFile) {
this._activeFile = activeFile;
if (this._activeFile.get() !== activeFile) {
this._activeFile.set(activeFile);
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ activeFile: true });
}
}
private _showWarnings: boolean = true;
private readonly _showWarnings = MarkersContextKeys.ShowWarningsFilterContextKey.bindTo(this.contextKeyService);
get showWarnings(): boolean {
return this._showWarnings;
return !!this._showWarnings.get();
}
set showWarnings(showWarnings: boolean) {
if (this._showWarnings !== showWarnings) {
this._showWarnings = showWarnings;
if (this._showWarnings.get() !== showWarnings) {
this._showWarnings.set(showWarnings);
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ showWarnings: true });
}
}
private _showErrors: boolean = true;
private readonly _showErrors = MarkersContextKeys.ShowErrorsFilterContextKey.bindTo(this.contextKeyService);
get showErrors(): boolean {
return this._showErrors;
return !!this._showErrors.get();
}
set showErrors(showErrors: boolean) {
if (this._showErrors !== showErrors) {
this._showErrors = showErrors;
if (this._showErrors.get() !== showErrors) {
this._showErrors.set(showErrors);
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ showErrors: true });
}
}
private _showInfos: boolean = true;
private readonly _showInfos = MarkersContextKeys.ShowInfoFilterContextKey.bindTo(this.contextKeyService);
get showInfos(): boolean {
return this._showInfos;
return !!this._showInfos.get();
}
set showInfos(showInfos: boolean) {
if (this._showInfos !== showInfos) {
this._showInfos = showInfos;
if (this._showInfos.get() !== showInfos) {
this._showInfos.set(showInfos);
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ showInfos: true });
}
}
private _layout: DOM.Dimension = new DOM.Dimension(0, 0);
get layout(): DOM.Dimension {
return this._layout;
}
set layout(layout: DOM.Dimension) {
if (this._layout.width !== layout.width || this._layout.height !== layout.height) {
this._layout = layout;
this._onDidChange.fire(<IMarkersFiltersChangeEvent>{ layout: true });
}
}
}
class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem {
constructor(
action: IAction, private filters: MarkersFilters, actionRunner: IActionRunner,
@IContextMenuService contextMenuService: IContextMenuService
) {
super(action,
{ getActions: () => this.getActions() },
contextMenuService,
{
actionRunner,
classNames: action.class,
anchorAlignmentProvider: () => AnchorAlignment.RIGHT,
menuAsChild: true
}
);
}
override render(container: HTMLElement): void {
super.render(container);
this.updateChecked();
}
private getActions(): IAction[] {
return [
{
checked: this.filters.showErrors,
class: undefined,
enabled: true,
id: 'showErrors',
label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_ERRORS,
run: async () => this.filters.showErrors = !this.filters.showErrors,
tooltip: ''
},
{
checked: this.filters.showWarnings,
class: undefined,
enabled: true,
id: 'showWarnings',
label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_WARNINGS,
run: async () => this.filters.showWarnings = !this.filters.showWarnings,
tooltip: ''
},
{
checked: this.filters.showInfos,
class: undefined,
enabled: true,
id: 'showInfos',
label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_INFOS,
run: async () => this.filters.showInfos = !this.filters.showInfos,
tooltip: ''
},
new Separator(),
{
checked: this.filters.activeFile,
class: undefined,
enabled: true,
id: 'activeFile',
label: Messages.MARKERS_PANEL_FILTER_LABEL_ACTIVE_FILE,
run: async () => this.filters.activeFile = !this.filters.activeFile,
tooltip: ''
},
{
checked: this.filters.excludedFiles,
class: undefined,
enabled: true,
id: 'useFilesExclude',
label: Messages.MARKERS_PANEL_FILTER_LABEL_EXCLUDED_FILES,
run: async () => this.filters.excludedFiles = !this.filters.excludedFiles,
tooltip: ''
},
];
}
override updateChecked(): void {
this.element!.classList.toggle('checked', this._action.checked);
}
}
const filterIcon = registerIcon('markers-view-filter', Codicon.filter, localize('filterIcon', 'Icon for the filter configuration in the markers view.'));
export class MarkersFilterActionViewItem extends BaseActionViewItem {
private delayedFilterUpdate: Delayer<void>;
private container: HTMLElement | null = null;
private filterInputBox: HistoryInputBox | null = null;
private filterBadge: HTMLElement | null = null;
private focusContextKey: IContextKey<boolean>;
private readonly filtersAction: IAction;
private actionbar: ActionBar | null = null;
private keybindingService;
constructor(
action: IAction,
private markersView: IMarkersView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService
) {
super(null, action);
this.keybindingService = keybindingService;
this.focusContextKey = MarkersContextKeys.MarkerViewFilterFocusContextKey.bindTo(contextKeyService);
this.delayedFilterUpdate = new Delayer<void>(400);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
this._register(markersView.onDidFocusFilter(() => this.focus()));
this._register(markersView.onDidClearFilterText(() => this.clearFilterText()));
this.filtersAction = new Action('markersFiltersAction', Messages.MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS, 'markers-filters ' + ThemeIcon.asClassName(filterIcon));
this.filtersAction.checked = this.hasFiltersChanged();
this._register(markersView.filters.onDidChange(e => this.onDidFiltersChange(e)));
}
override render(container: HTMLElement): void {
this.container = container;
this.container.classList.add('markers-panel-action-filter-container');
this.element = DOM.append(this.container, DOM.$(''));
this.element.className = this.class;
this.createInput(this.element);
this.createControls(this.element);
this.updateClass();
this.adjustInputBox();
}
override focus(): void {
this.filterInputBox?.focus();
}
override blur(): void {
this.filterInputBox?.blur();
}
override setFocusable(): void {
// noop input elements are focusable by default
}
override get trapsArrowNavigation(): boolean {
return true;
}
private clearFilterText(): void {
if (this.filterInputBox) {
this.filterInputBox.value = '';
}
}
private onDidFiltersChange(e: IMarkersFiltersChangeEvent): void {
this.filtersAction.checked = this.hasFiltersChanged();
if (e.layout) {
this.updateClass();
}
}
private hasFiltersChanged(): boolean {
return !this.markersView.filters.showErrors || !this.markersView.filters.showWarnings || !this.markersView.filters.showInfos || this.markersView.filters.excludedFiles || this.markersView.filters.activeFile;
}
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER,
ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL,
history: this.markersView.filters.filterHistory,
showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService)
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.markersView.filters.filterText;
this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!))));
this._register(this.markersView.filters.onDidChange((event: IMarkersFiltersChangeEvent) => {
if (event.filterText) {
this.filterInputBox!.value = this.markersView.filters.filterText;
}
}));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!)));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => {
e.stopPropagation();
e.preventDefault();
}));
const focusTracker = this._register(DOM.trackFocus(this.filterInputBox.inputElement));
this._register(focusTracker.onDidFocus(() => this.focusContextKey.set(true)));
this._register(focusTracker.onDidBlur(() => this.focusContextKey.set(false)));
this._register(toDisposable(() => this.focusContextKey.reset()));
}
private createControls(container: HTMLElement): void {
const controlsContainer = DOM.append(container, DOM.$('.markers-panel-filter-controls'));
this.createBadge(controlsContainer);
this.createFilters(controlsContainer);
}
private createBadge(container: HTMLElement): void {
const filterBadge = this.filterBadge = DOM.append(container, DOM.$('.markers-panel-filter-badge'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
filterBadge.style.backgroundColor = background;
filterBadge.style.borderWidth = border ? '1px' : '';
filterBadge.style.borderStyle = border ? 'solid' : '';
filterBadge.style.borderColor = border;
filterBadge.style.color = foreground;
}));
this.updateBadge();
this._register(this.markersView.onDidChangeFilterStats(() => this.updateBadge()));
}
private createFilters(container: HTMLElement): void {
this.actionbar = this._register(new ActionBar(container, {
actionViewItemProvider: action => {
if (action.id === this.filtersAction.id) {
return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.markersView.filters, this.actionRunner);
}
return undefined;
}
}));
this.actionbar.push(this.filtersAction, { icon: true, label: false });
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this.markersView.filters.filterText = inputbox.value;
this.markersView.filters.filterHistory = inputbox.getHistory();
}
private updateBadge(): void {
if (this.filterBadge) {
const { total, filtered } = this.markersView.getFilterStats();
this.filterBadge.classList.toggle('hidden', total === filtered || total === 0);
this.filterBadge.textContent = localize('showing filtered problems', "Showing {0} of {1}", filtered, total);
this.adjustInputBox();
}
}
private adjustInputBox(): void {
if (this.element && this.filterInputBox && this.filterBadge) {
this.filterInputBox.inputElement.style.paddingRight = this.element.classList.contains('small') || this.filterBadge.classList.contains('hidden') ? '25px' : '150px';
}
}
// Action toolbar is swallowing some keys for action items which should not be for an input box
private handleKeyboardEvent(event: StandardKeyboardEvent) {
if (event.equals(KeyCode.Space)
|| event.equals(KeyCode.LeftArrow)
|| event.equals(KeyCode.RightArrow)
) {
event.stopPropagation();
}
}
private onInputKeyDown(event: StandardKeyboardEvent, filterInputBox: HistoryInputBox) {
let handled = false;
if (event.equals(KeyCode.Tab)) {
this.actionbar?.focus();
handled = true;
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
protected override updateClass(): void {
if (this.element && this.container) {
this.element.className = this.class;
this.container.classList.toggle('grow', this.element.classList.contains('grow'));
this.adjustInputBox();
}
}
protected get class(): string {
if (this.markersView.filters.layout.width > 600) {
return 'markers-panel-action-filter grow';
} else if (this.markersView.filters.layout.width < 400) {
return 'markers-panel-action-filter small';
} else {
return 'markers-panel-action-filter';
}
}
}
export class QuickFixAction extends Action {

View file

@ -3,67 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar .action-item.markers-panel-action-filter-container {
cursor: default;
display: flex;
}
.monaco-action-bar .markers-panel-action-filter {
display: flex;
align-items: center;
flex: 1;
}
.monaco-action-bar .markers-panel-action-filter .monaco-inputbox {
height: 24px;
font-size: 12px;
flex: 1;
}
.pane-header .monaco-action-bar .markers-panel-action-filter .monaco-inputbox {
height: 20px;
line-height: 18px;
}
.monaco-workbench.vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox {
height: 25px;
}
.markers-panel-action-filter > .markers-panel-filter-controls {
position: absolute;
top: 0px;
bottom: 0;
right: 0px;
display: flex;
align-items: center;
}
.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge {
margin: 4px 0px;
padding: 0px 8px;
border-radius: 2px;
}
.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.hidden,
.markers-panel-action-filter.small > .markers-panel-filter-controls > .markers-panel-filter-badge {
display: none;
}
.markers-panel-action-filter > .markers-panel-filter-controls > .monaco-action-bar .action-item .action-label.codicon.markers-filters {
padding: 2px;
}
.panel > .title .monaco-action-bar .action-item.markers-panel-action-filter-container {
max-width: 400px;
min-width: 300px;
margin-right: 10px;
}
.markers-panel-container .monaco-action-bar.markers-panel-filter-container .action-item.markers-panel-action-filter-container,
.panel > .title .monaco-action-bar .action-item.markers-panel-action-filter-container.grow {
flex: 1;
}
.markers-panel .markers-panel-container {
height: 100%;
}
@ -72,11 +11,6 @@
display: none;
}
.markers-panel-container .monaco-action-bar.markers-panel-filter-container {
margin: 10px 20px;
height: initial;
}
.markers-panel .markers-panel-container .message-box-container {
line-height: 22px;
padding-left: 20px;

View file

@ -31,9 +31,13 @@ export namespace Markers {
export namespace MarkersContextKeys {
export const MarkersViewModeContextKey = new RawContextKey<MarkersViewMode>('problemsViewMode', MarkersViewMode.Tree);
export const MarkersViewSmallLayoutContextKey = new RawContextKey<boolean>(`problemsView.smallLayout`, false);
export const MarkersTreeVisibilityContextKey = new RawContextKey<boolean>('problemsVisibility', false);
export const MarkerFocusContextKey = new RawContextKey<boolean>('problemFocus', false);
export const MarkerViewFilterFocusContextKey = new RawContextKey<boolean>('problemsFilterFocus', false);
export const RelatedInformationFocusContextKey = new RawContextKey<boolean>('relatedInformationFocus', false);
export const ShowErrorsFilterContextKey = new RawContextKey<boolean>('problems.filter.errors', true);
export const ShowWarningsFilterContextKey = new RawContextKey<boolean>('problems.filter.warnings', true);
export const ShowInfoFilterContextKey = new RawContextKey<boolean>('problems.filter.info', true);
export const ShowActiveFileFilterContextKey = new RawContextKey<boolean>('problems.filter.activeFile', false);
export const ShowExcludedFilesFilterContextKey = new RawContextKey<boolean>('problems.filter.excludedFiles', true);
}