未验证 提交 4b904130 编写于 作者: I Isidor Nikolic 提交者: GitHub

Merge pull request #102704 from arhelmus/master

Implemented filter for debug console output
......@@ -59,12 +59,14 @@ import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor';
import { TreeFilterPanelActionViewItem, TreeFilterState } from 'vs/workbench/contrib/treeFilter/browser/treeFilterView';
import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter';
const $ = dom.$;
const HISTORY_STORAGE_KEY = 'debug.repl.history';
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;
......@@ -93,6 +95,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
private styleElement: HTMLStyleElement | undefined;
private completionItemProvider: IDisposable | undefined;
private modelChangeListener: IDisposable = Disposable.None;
private filter: ReplFilter;
private filterState: TreeFilterState;
constructor(
options: IViewPaneOptions,
......@@ -116,6 +120,13 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50);
this.filter = new ReplFilter();
this.filterState = this._register(new TreeFilterState({
filterText: '',
filterHistory: [],
layout: new dom.Dimension(0, 0),
}));
codeEditorService.registerDecorationType(DECORATION_KEY, {});
this.registerListeners();
}
......@@ -237,6 +248,15 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this._register(this.editorService.onDidActiveEditorChange(() => {
this.setMode();
}));
this._register(this.filterState.onDidChange((e) => {
if (e.filterText) {
this.filter.filterQuery = this.filterState.filterText;
if (this.tree) {
this.tree.refilter();
}
}
}));
}
get isReadonly(): boolean {
......@@ -428,6 +448,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.replInputContainer.style.height = `${replInputHeight}px`;
this.replInput.layout({ width: width - 30, height: replInputHeight });
this.filterState.layout = new dom.Dimension(width, height);
}
focus(): void {
......@@ -437,6 +458,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
getActionViewItem(action: IAction): IActionViewItem | undefined {
if (action.id === SelectReplAction.ID) {
return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction);
} else if (action.id === FILTER_ACTION_ID) {
return this.instantiationService.createInstance(TreeFilterPanelActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter. E.g.: text, !exclude"), this.filterState);
}
return super.getActionViewItem(action);
......@@ -444,6 +467,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
getActions(): IAction[] {
const result: IAction[] = [];
result.push(new Action(FILTER_ACTION_ID));
if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) {
result.push(this.selectReplAction);
}
......@@ -532,6 +556,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
// https://github.com/microsoft/TypeScript/issues/32526
new ReplDataSource() as IAsyncDataSource<IDebugSession, IReplElement>,
{
filter: this.filter,
accessibilityProvider: new ReplAccessibilityProvider(),
identityProvider: { getId: (element: IReplElement) => element.getId() },
mouseSupport: false,
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { matchesFuzzy } from 'vs/base/common/filters';
import { splitGlobAware } from 'vs/base/common/glob';
import * as strings from 'vs/base/common/strings';
import { ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
import { IReplElement } from 'vs/workbench/contrib/debug/common/debug';
type ParsedQuery = {
type: 'include' | 'exclude',
query: string,
};
export class ReplFilter implements ITreeFilter<IReplElement> {
static matchQuery = matchesFuzzy;
private _parsedQueries: ParsedQuery[] = [];
set filterQuery(query: string) {
this._parsedQueries = [];
query = query.trim();
if (query && query !== '') {
const filters = splitGlobAware(query, ',').map(s => s.trim()).filter(s => !!s.length);
for (const f of filters) {
if (strings.startsWith(f, '!')) {
this._parsedQueries.push({ type: 'exclude', query: f.slice(1) });
} else {
this._parsedQueries.push({ type: 'include', query: f });
}
}
}
}
filter(element: IReplElement, parentVisibility: TreeVisibility): TreeFilterResult<void> {
if (this._parsedQueries.length === 0) {
return parentVisibility;
}
let includeQueryPresent = false;
let includeQueryMatched = false;
const text = element.toString();
for (let { type, query } of this._parsedQueries) {
if (type === 'exclude' && ReplFilter.matchQuery(query, text)) {
// If exclude query matches, ignore all other queries and hide
return false;
} else if (type === 'include') {
includeQueryPresent = true;
if (ReplFilter.matchQuery(query, text)) {
includeQueryMatched = true;
}
}
}
return includeQueryPresent ? includeQueryMatched : parentVisibility;
}
}
......@@ -174,13 +174,18 @@ export class ReplGroup implements IReplElement {
}
}
type FilterFunc = ((element: IReplElement) => void);
export class ReplModel {
private replElements: IReplElement[] = [];
private readonly _onDidChangeElements = new Emitter<void>();
readonly onDidChangeElements = this._onDidChangeElements.event;
private filterFunc: FilterFunc | undefined;
getReplElements(): IReplElement[] {
return this.replElements;
return this.replElements.filter(element =>
this.filterFunc ? this.filterFunc(element) : true
);
}
async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise<void> {
......@@ -315,6 +320,10 @@ export class ReplModel {
}
}
setFilter(filterFunc: FilterFunc): void {
this.filterFunc = filterFunc;
}
removeReplExpressions(): void {
if (this.replElements.length > 0) {
this.replElements = [];
......
......@@ -12,6 +12,8 @@ import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel
import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession';
import { timeout } from 'vs/base/common/async';
import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test';
import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter';
import { TreeVisibility } from 'vs/base/browser/ui/tree/tree';
suite('Debug - REPL', () => {
let model: DebugModel;
......@@ -189,4 +191,59 @@ suite('Debug - REPL', () => {
assert.equal(repl.getReplElements().length, 3);
assert.equal((<SimpleReplElement>repl.getReplElements()[2]).value, 'second global line');
});
test('repl filter', async () => {
const session = createMockSession(model);
const repl = new ReplModel();
const replFilter = new ReplFilter();
repl.setFilter((element) => {
const filterResult = replFilter.filter(element, TreeVisibility.Visible);
return filterResult === true || filterResult === TreeVisibility.Visible;
});
repl.appendToRepl(session, 'first line\n', severity.Info);
repl.appendToRepl(session, 'second line\n', severity.Info);
repl.appendToRepl(session, 'third line\n', severity.Info);
repl.appendToRepl(session, 'fourth line\n', severity.Info);
replFilter.filterQuery = 'first';
let r1 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r1.length, 1);
assert.equal(r1[0].value, 'first line\n');
replFilter.filterQuery = '!first';
let r2 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r1.length, 1);
assert.equal(r2[0].value, 'second line\n');
assert.equal(r2[1].value, 'third line\n');
assert.equal(r2[2].value, 'fourth line\n');
replFilter.filterQuery = 'first, line';
let r3 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r3.length, 4);
assert.equal(r3[0].value, 'first line\n');
assert.equal(r3[1].value, 'second line\n');
assert.equal(r3[2].value, 'third line\n');
assert.equal(r3[3].value, 'fourth line\n');
replFilter.filterQuery = 'line, !second';
let r4 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r4.length, 3);
assert.equal(r4[0].value, 'first line\n');
assert.equal(r4[1].value, 'third line\n');
assert.equal(r4[2].value, 'fourth line\n');
replFilter.filterQuery = '!second, line';
let r4_same = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r4.length, r4_same.length);
replFilter.filterQuery = '!line';
let r5 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r5.length, 0);
replFilter.filterQuery = 'smth';
let r6 = <SimpleReplElement[]>repl.getReplElements();
assert.equal(r6.length, 0);
});
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar .action-item.panel-action-tree-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.panel-action-tree-filter-container {
max-width: 600px;
min-width: 300px;
margin-right: 10px;
}
.monaco-action-bar .action-item.panel-action-tree-filter-container,
.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container.grow {
flex: 1;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/treeFilter';
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, Disposable } 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/browser/contextScopedHistoryWidget';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export interface IReplFiltersChangeEvent {
filterText?: boolean;
layout?: boolean;
}
export interface IReplFiltersOptions {
filterText: string;
filterHistory: string[];
layout: DOM.Dimension;
}
export class TreeFilterState extends Disposable {
private readonly _onDidChange: Emitter<IReplFiltersChangeEvent> = this._register(new Emitter<IReplFiltersChangeEvent>());
readonly onDidChange: Event<IReplFiltersChangeEvent> = this._onDidChange.event;
constructor(options: IReplFiltersOptions) {
super();
this._filterText = options.filterText;
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 });
}
}
filterHistory: string[];
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(<IReplFiltersChangeEvent>{ layout: true });
}
}
}
export class TreeFilterPanelActionViewItem extends BaseActionViewItem {
private delayedFilterUpdate: Delayer<void>;
private container: HTMLElement | undefined;
private filterInputBox: HistoryInputBox | undefined;
constructor(
action: IAction,
private placeholder: string,
private filters: TreeFilterState,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService) {
super(null, action);
this.delayedFilterUpdate = new Delayer<void>(200);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
}
render(container: HTMLElement): void {
this.container = container;
DOM.addClass(this.container, 'panel-action-tree-filter-container');
this.element = DOM.append(this.container, DOM.$(''));
this.element.className = this.class;
this.createInput(this.element);
this.updateClass();
this.adjustInputBox();
}
focus(): void {
if (this.filterInputBox) {
this.filterInputBox.focus();
}
}
private clearFilterText(): void {
if (this.filterInputBox) {
this.filterInputBox.value = '';
}
}
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: this.placeholder,
history: this.filters.filterHistory
}));
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((event: IReplFiltersChangeEvent) => {
if (event.filterText) {
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();
}));
this._register(this.filters.onDidChange(e => this.onDidFiltersChange(e)));
}
private onDidFiltersChange(e: IReplFiltersChangeEvent): void {
if (e.layout) {
this.updateClass();
}
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this.filters.filterText = inputbox.value;
this.filters.filterHistory = inputbox.getHistory();
}
// 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 adjustInputBox(): void {
if (this.element && this.filterInputBox) {
this.filterInputBox.inputElement.style.paddingRight = DOM.hasClass(this.element, 'small') ? '25px' : '150px';
}
}
protected updateClass(): void {
if (this.element && this.container) {
this.element.className = this.class;
DOM.toggleClass(this.container, 'grow', DOM.hasClass(this.element, 'grow'));
this.adjustInputBox();
}
}
protected get class(): string {
if (this.filters.layout.width > 800) {
return 'panel-action-tree-filter grow';
} else if (this.filters.layout.width < 600) {
return 'panel-action-tree-filter small';
} else {
return 'panel-action-tree-filter';
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册