mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user