Output filter: support negative and multiple filters (#283595)

* Initial plan

* Implement multiple and negative filters for Output panel

Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com>

* Update filter placeholder with examples

Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com>

* Add comment to test explaining duplication

Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com>

* remove test

* improvise

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
This commit is contained in:
Copilot
2025-12-17 22:46:58 +00:00
committed by GitHub
parent a808643c3a
commit dfc25ccd2b
3 changed files with 119 additions and 13 deletions

View File

@@ -132,15 +132,91 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters {
filterHistory: string[];
private _filterText = '';
private _includePatterns: string[] = [];
private _excludePatterns: string[] = [];
get text(): string {
return this._filterText;
}
set text(filterText: string) {
if (this._filterText !== filterText) {
this._filterText = filterText;
const { includePatterns, excludePatterns } = this.parseText(filterText);
this._includePatterns = includePatterns;
this._excludePatterns = excludePatterns;
this._onDidChange.fire();
}
}
private parseText(filterText: string): { includePatterns: string[]; excludePatterns: string[] } {
const includePatterns: string[] = [];
const excludePatterns: string[] = [];
// Parse patterns respecting quoted strings
const patterns = this.splitByCommaRespectingQuotes(filterText);
for (const pattern of patterns) {
const trimmed = pattern.trim();
if (trimmed.length === 0) {
continue;
}
if (trimmed.startsWith('!')) {
// Negative filter - remove the ! prefix
const negativePattern = trimmed.substring(1).trim();
if (negativePattern.length > 0) {
excludePatterns.push(negativePattern);
}
} else {
includePatterns.push(trimmed);
}
}
return { includePatterns, excludePatterns };
}
get includePatterns(): string[] {
return this._includePatterns;
}
get excludePatterns(): string[] {
return this._excludePatterns;
}
private splitByCommaRespectingQuotes(text: string): string[] {
const patterns: string[] = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (!inQuotes && (char === '"')) {
// Start of quoted string
inQuotes = true;
quoteChar = char;
current += char;
} else if (inQuotes && char === quoteChar) {
// End of quoted string
inQuotes = false;
current += char;
} else if (!inQuotes && char === ',') {
// Comma outside quotes - split here
if (current.length > 0) {
patterns.push(current);
}
current = '';
} else {
current += char;
}
}
// Add the last pattern
if (current.length > 0) {
patterns.push(current);
}
return patterns;
}
private readonly _trace: IContextKey<boolean>;
get trace(): boolean {

View File

@@ -92,7 +92,7 @@ export class OutputViewPane extends FilterViewPane {
super({
...options,
filterOptions: {
placeholder: localize('outputView.filter.placeholder', "Filter"),
placeholder: localize('outputView.filter.placeholder', "Filter (e.g. text, !excludeText, text1,text2)"),
focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key,
text: viewState.filter || '',
history: []
@@ -466,6 +466,38 @@ export class FilterController extends Disposable implements IEditorContribution
}
}
private shouldShowLine(model: ITextModel, range: Range, positive: string[], negative: string[]): { show: boolean; matches: IModelDeltaDecoration[] } {
const matches: IModelDeltaDecoration[] = [];
// Check negative filters first - if any match, hide the line
if (negative.length > 0) {
for (const pattern of negative) {
const negativeMatches = model.findMatches(pattern, range, false, false, null, false);
if (negativeMatches.length > 0) {
return { show: false, matches: [] };
}
}
}
// If there are positive filters, at least one must match
if (positive.length > 0) {
let hasPositiveMatch = false;
for (const pattern of positive) {
const positiveMatches = model.findMatches(pattern, range, false, false, null, false);
if (positiveMatches.length > 0) {
hasPositiveMatch = true;
for (const match of positiveMatches) {
matches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
}
}
}
return { show: hasPositiveMatch, matches };
}
// No positive filters means show everything (that passed negative filters)
return { show: true, matches };
}
private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map<string, string> } {
const filters = this.outputService.filters;
const activeChannel = this.outputService.getActiveChannel();
@@ -495,12 +527,10 @@ export class FilterController extends Disposable implements IEditorContribution
hiddenAreas.push(entry.range);
continue;
}
if (filters.text) {
const matches = model.findMatches(filters.text, entry.range, false, false, null, false);
if (matches.length) {
for (const match of matches) {
findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
}
if (filters.includePatterns.length > 0 || filters.excludePatterns.length > 0) {
const result = this.shouldShowLine(model, entry.range, filters.includePatterns, filters.excludePatterns);
if (result.show) {
findMatches.push(...result.matches);
} else {
hiddenAreas.push(entry.range);
}
@@ -509,18 +539,16 @@ export class FilterController extends Disposable implements IEditorContribution
return { findMatches, hiddenAreas, categories };
}
if (!filters.text) {
if (filters.includePatterns.length === 0 && filters.excludePatterns.length === 0) {
return { findMatches, hiddenAreas, categories };
}
const lineCount = model.getLineCount();
for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) {
const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));
const matches = model.findMatches(filters.text, lineRange, false, false, null, false);
if (matches.length) {
for (const match of matches) {
findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
}
const result = this.shouldShowLine(model, lineRange, filters.includePatterns, filters.excludePatterns);
if (result.show) {
findMatches.push(...result.matches);
} else {
hiddenAreas.push(lineRange);
}

View File

@@ -56,6 +56,8 @@ export const HIDE_CATEGORY_FILTER_CONTEXT = new RawContextKey<string>('output.fi
export interface IOutputViewFilters {
readonly onDidChange: Event<void>;
text: string;
readonly includePatterns: string[];
readonly excludePatterns: string[];
trace: boolean;
debug: boolean;
info: boolean;