diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 16f12b6801b..9d605b386b0 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -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; get trace(): boolean { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 1f70bf8856e..2008f568448 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -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 } { 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); } diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 00a11a6f566..9b297e69ef3 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -56,6 +56,8 @@ export const HIDE_CATEGORY_FILTER_CONTEXT = new RawContextKey('output.fi export interface IOutputViewFilters { readonly onDidChange: Event; text: string; + readonly includePatterns: string[]; + readonly excludePatterns: string[]; trace: boolean; debug: boolean; info: boolean;