Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/output/browser/outputView.ts
5252 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import './output.css';
6
import * as nls from '../../../../nls.js';
7
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
8
import { IEditorOptions as ICodeEditorOptions } from '../../../../editor/common/config/editorOptions.js';
9
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
10
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
11
import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
12
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IContextKeyService, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
14
import { IEditorOpenContext } from '../../../common/editor.js';
15
import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js';
16
import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, ILogEntry, HIDE_CATEGORY_FILTER_CONTEXT } from '../../../services/output/common/output.js';
17
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
18
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
19
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
20
import { CancellationToken } from '../../../../base/common/cancellation.js';
21
import { IEditorService } from '../../../services/editor/common/editorService.js';
22
import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';
23
import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js';
24
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
25
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
26
import { IViewDescriptorService } from '../../../common/views.js';
27
import { TextResourceEditorInput } from '../../../common/editor/textResourceEditorInput.js';
28
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
29
import { Dimension } from '../../../../base/browser/dom.js';
30
import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
31
import { CancelablePromise, createCancelablePromise } from '../../../../base/common/async.js';
32
import { IFileService } from '../../../../platform/files/common/files.js';
33
import { ResourceContextKey } from '../../../common/contextkeys.js';
34
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
35
import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js';
36
import { computeEditorAriaLabel } from '../../../browser/editor.js';
37
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
38
import { localize } from '../../../../nls.js';
39
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
40
import { LogLevel } from '../../../../platform/log/common/log.js';
41
import { IEditorContributionDescription, EditorExtensionsRegistry, EditorContributionInstantiation, EditorContributionCtor } from '../../../../editor/browser/editorExtensions.js';
42
import { ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
43
import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';
44
import { IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js';
45
import { Range } from '../../../../editor/common/core/range.js';
46
import { FindDecorations } from '../../../../editor/contrib/find/browser/findDecorations.js';
47
import { Memento } from '../../../common/memento.js';
48
import { Markers } from '../../markers/common/markers.js';
49
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
50
import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js';
51
import { escapeRegExpCharacters } from '../../../../base/common/strings.js';
52
53
interface IOutputViewState {
54
filter?: string;
55
showTrace?: boolean;
56
showDebug?: boolean;
57
showInfo?: boolean;
58
showWarning?: boolean;
59
showError?: boolean;
60
categories?: string;
61
}
62
63
export class OutputViewPane extends FilterViewPane {
64
65
private readonly editor: OutputEditor;
66
private channelId: string | undefined;
67
private editorPromise: CancelablePromise<void> | null = null;
68
69
private readonly scrollLockContextKey: IContextKey<boolean>;
70
get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); }
71
set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); }
72
73
private readonly memento: Memento<IOutputViewState>;
74
private readonly panelState: IOutputViewState;
75
76
constructor(
77
options: IViewPaneOptions,
78
@IKeybindingService keybindingService: IKeybindingService,
79
@IContextMenuService contextMenuService: IContextMenuService,
80
@IConfigurationService configurationService: IConfigurationService,
81
@IContextKeyService contextKeyService: IContextKeyService,
82
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
83
@IInstantiationService instantiationService: IInstantiationService,
84
@IOpenerService openerService: IOpenerService,
85
@IThemeService themeService: IThemeService,
86
@IHoverService hoverService: IHoverService,
87
@IOutputService private readonly outputService: IOutputService,
88
@IStorageService storageService: IStorageService,
89
) {
90
const memento = new Memento<IOutputViewState>(Markers.MARKERS_VIEW_STORAGE_ID, storageService);
91
const viewState = memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
92
super({
93
...options,
94
filterOptions: {
95
placeholder: localize('outputView.filter.placeholder', "Filter (e.g. text, !excludeText, text1,text2)"),
96
focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key,
97
text: viewState.filter || '',
98
history: []
99
}
100
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
101
this.memento = memento;
102
this.panelState = viewState;
103
104
const filters = outputService.filters;
105
filters.text = this.panelState.filter || '';
106
filters.trace = this.panelState.showTrace ?? true;
107
filters.debug = this.panelState.showDebug ?? true;
108
filters.info = this.panelState.showInfo ?? true;
109
filters.warning = this.panelState.showWarning ?? true;
110
filters.error = this.panelState.showError ?? true;
111
filters.categories = this.panelState.categories ?? '';
112
113
this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService);
114
115
const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
116
this.editor = this._register(editorInstantiationService.createInstance(OutputEditor));
117
this._register(this.editor.onTitleAreaUpdate(() => {
118
this.updateTitle(this.editor.getTitle());
119
this.updateActions();
120
}));
121
this._register(this.onDidChangeBodyVisibility(() => this.onDidChangeVisibility(this.isBodyVisible())));
122
this._register(this.filterWidget.onDidChangeFilterText(text => outputService.filters.text = text));
123
124
this.checkMoreFilters();
125
this._register(outputService.filters.onDidChange(() => this.checkMoreFilters()));
126
}
127
128
showChannel(channel: IOutputChannel, preserveFocus: boolean): void {
129
if (this.channelId !== channel.id) {
130
this.setInput(channel);
131
}
132
if (!preserveFocus) {
133
this.focus();
134
}
135
}
136
137
override focus(): void {
138
super.focus();
139
this.editorPromise?.then(() => this.editor.focus());
140
}
141
142
public clearFilterText(): void {
143
this.filterWidget.setFilterText('');
144
}
145
146
protected override renderBody(container: HTMLElement): void {
147
super.renderBody(container);
148
this.editor.create(container);
149
container.classList.add('output-view');
150
const codeEditor = <ICodeEditor>this.editor.getControl();
151
codeEditor.setAriaOptions({ role: 'document', activeDescendant: undefined });
152
this._register(codeEditor.onDidChangeModelContent(() => {
153
if (!this.scrollLock) {
154
this.editor.revealLastLine();
155
}
156
}));
157
this._register(codeEditor.onDidChangeCursorPosition((e) => {
158
if (e.reason !== CursorChangeReason.Explicit) {
159
return;
160
}
161
162
if (!this.configurationService.getValue('output.smartScroll.enabled')) {
163
return;
164
}
165
166
const model = codeEditor.getModel();
167
if (model) {
168
const newPositionLine = e.position.lineNumber;
169
const lastLine = model.getLineCount();
170
this.scrollLock = lastLine !== newPositionLine;
171
}
172
}));
173
}
174
175
protected layoutBodyContent(height: number, width: number): void {
176
this.editor.layout(new Dimension(width, height));
177
}
178
179
private onDidChangeVisibility(visible: boolean): void {
180
this.editor.setVisible(visible);
181
if (!visible) {
182
this.clearInput();
183
}
184
}
185
186
private setInput(channel: IOutputChannel): void {
187
this.channelId = channel.id;
188
this.checkMoreFilters();
189
190
const input = this.createInput(channel);
191
if (!this.editor.input || !input.matches(this.editor.input)) {
192
this.editorPromise?.cancel();
193
this.editorPromise = createCancelablePromise(token => this.editor.setInput(input, { preserveFocus: true }, Object.create(null), token));
194
}
195
196
}
197
198
private checkMoreFilters(): void {
199
const filters = this.outputService.filters;
200
this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.categories.includes(`,${this.channelId}:`)));
201
}
202
203
private clearInput(): void {
204
this.channelId = undefined;
205
this.editor.clearInput();
206
this.editorPromise = null;
207
}
208
209
private createInput(channel: IOutputChannel): TextResourceEditorInput {
210
return this.instantiationService.createInstance(TextResourceEditorInput, channel.uri, nls.localize('output model title', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), undefined, undefined);
211
}
212
213
override saveState(): void {
214
const filters = this.outputService.filters;
215
this.panelState.filter = filters.text;
216
this.panelState.showTrace = filters.trace;
217
this.panelState.showDebug = filters.debug;
218
this.panelState.showInfo = filters.info;
219
this.panelState.showWarning = filters.warning;
220
this.panelState.showError = filters.error;
221
this.panelState.categories = filters.categories;
222
223
this.memento.saveMemento();
224
super.saveState();
225
}
226
227
}
228
229
export class OutputEditor extends AbstractTextResourceEditor {
230
private readonly resourceContext: ResourceContextKey;
231
232
constructor(
233
@ITelemetryService telemetryService: ITelemetryService,
234
@IInstantiationService instantiationService: IInstantiationService,
235
@IStorageService storageService: IStorageService,
236
@IConfigurationService private readonly configurationService: IConfigurationService,
237
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
238
@IThemeService themeService: IThemeService,
239
@IEditorGroupsService editorGroupService: IEditorGroupsService,
240
@IEditorService editorService: IEditorService,
241
@IFileService fileService: IFileService
242
) {
243
super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* this is not correct but pragmatic */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService);
244
245
this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));
246
}
247
248
override getId(): string {
249
return OUTPUT_VIEW_ID;
250
}
251
252
override getTitle(): string {
253
return nls.localize('output', "Output");
254
}
255
256
protected override getConfigurationOverrides(configuration: IEditorConfiguration): ICodeEditorOptions {
257
const options = super.getConfigurationOverrides(configuration);
258
options.wordWrap = 'on'; // all output editors wrap
259
options.lineNumbers = 'off'; // all output editors hide line numbers
260
options.glyphMargin = false;
261
options.lineDecorationsWidth = 20;
262
options.rulers = [];
263
options.folding = false;
264
options.scrollBeyondLastLine = false;
265
options.renderLineHighlight = 'none';
266
options.minimap = { enabled: false };
267
options.renderValidationDecorations = 'editable';
268
options.padding = undefined;
269
options.readOnly = true;
270
options.domReadOnly = true;
271
options.roundedSelection = false;
272
options.unicodeHighlight = {
273
nonBasicASCII: false,
274
invisibleCharacters: false,
275
ambiguousCharacters: false,
276
};
277
278
const outputConfig = this.configurationService.getValue<{ 'editor.minimap.enabled'?: boolean; 'editor.wordWrap'?: 'off' | 'on' | 'wordWrapColumn' | 'bounded' }>('[Log]');
279
if (outputConfig) {
280
if (outputConfig['editor.minimap.enabled']) {
281
options.minimap = { enabled: true };
282
}
283
if (outputConfig['editor.wordWrap']) {
284
options.wordWrap = outputConfig['editor.wordWrap'];
285
}
286
}
287
288
return options;
289
}
290
291
protected getAriaLabel(): string {
292
return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel");
293
}
294
295
protected override computeAriaLabel(): string {
296
return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel();
297
}
298
299
override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
300
const focus = !(options && options.preserveFocus);
301
if (this.input && input.matches(this.input)) {
302
return;
303
}
304
305
if (this.input) {
306
// Dispose previous input (Output panel is not a workbench editor)
307
this.input.dispose();
308
}
309
await super.setInput(input, options, context, token);
310
311
this.resourceContext.set(input.resource);
312
313
if (focus) {
314
this.focus();
315
}
316
this.revealLastLine();
317
}
318
319
override clearInput(): void {
320
if (this.input) {
321
// Dispose current input (Output panel is not a workbench editor)
322
this.input.dispose();
323
}
324
super.clearInput();
325
326
this.resourceContext.reset();
327
}
328
329
protected override createEditor(parent: HTMLElement): void {
330
331
parent.setAttribute('role', 'document');
332
333
super.createEditor(parent);
334
335
const scopedContextKeyService = this.scopedContextKeyService;
336
if (scopedContextKeyService) {
337
CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true);
338
}
339
}
340
341
private _getContributions(): IEditorContributionDescription[] {
342
return [
343
...EditorExtensionsRegistry.getEditorContributions(),
344
{
345
id: FilterController.ID,
346
ctor: FilterController as EditorContributionCtor,
347
instantiation: EditorContributionInstantiation.Eager
348
}
349
];
350
}
351
352
protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {
353
return { contributions: this._getContributions() };
354
}
355
356
}
357
358
export class FilterController extends Disposable implements IEditorContribution {
359
360
public static readonly ID = 'output.editor.contrib.filterController';
361
362
private readonly modelDisposables: DisposableStore = this._register(new DisposableStore());
363
private hiddenAreas: Range[] = [];
364
private readonly categories = new Map<string, string>();
365
private readonly decorationsCollection: IEditorDecorationsCollection;
366
367
constructor(
368
private readonly editor: ICodeEditor,
369
@IOutputService private readonly outputService: IOutputService,
370
) {
371
super();
372
this.decorationsCollection = editor.createDecorationsCollection();
373
this._register(editor.onDidChangeModel(() => this.onDidChangeModel()));
374
this._register(this.outputService.filters.onDidChange(() => editor.hasModel() && this.filter(editor.getModel())));
375
}
376
377
private onDidChangeModel(): void {
378
this.modelDisposables.clear();
379
this.hiddenAreas = [];
380
this.categories.clear();
381
382
if (!this.editor.hasModel()) {
383
return;
384
}
385
386
const model = this.editor.getModel();
387
this.filter(model);
388
389
const computeEndLineNumber = () => {
390
const endLineNumber = model.getLineCount();
391
return endLineNumber > 1 && model.getLineMaxColumn(endLineNumber) === 1 ? endLineNumber - 1 : endLineNumber;
392
};
393
394
let endLineNumber = computeEndLineNumber();
395
396
this.modelDisposables.add(model.onDidChangeContent(e => {
397
if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) {
398
this.filterIncremental(model, endLineNumber + 1);
399
} else {
400
this.filter(model);
401
}
402
endLineNumber = computeEndLineNumber();
403
}));
404
}
405
406
private filter(model: ITextModel): void {
407
this.hiddenAreas = [];
408
this.decorationsCollection.clear();
409
this.filterIncremental(model, 1);
410
}
411
412
private filterIncremental(model: ITextModel, fromLineNumber: number): void {
413
const { findMatches, hiddenAreas, categories: sources } = this.compute(model, fromLineNumber);
414
this.hiddenAreas.push(...hiddenAreas);
415
this.editor.setHiddenAreas(this.hiddenAreas, this);
416
if (findMatches.length) {
417
this.decorationsCollection.append(findMatches);
418
}
419
if (sources.size) {
420
const that = this;
421
for (const [categoryFilter, categoryName] of sources) {
422
if (this.categories.has(categoryFilter)) {
423
continue;
424
}
425
this.categories.set(categoryFilter, categoryName);
426
this.modelDisposables.add(registerAction2(class extends Action2 {
427
constructor() {
428
super({
429
id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${categoryFilter}`,
430
title: categoryName,
431
toggled: ContextKeyExpr.regex(HIDE_CATEGORY_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(categoryFilter)},.*`)).negate(),
432
menu: {
433
id: viewFilterSubmenu,
434
group: '1_category_filter',
435
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)),
436
}
437
});
438
}
439
async run(): Promise<void> {
440
that.outputService.filters.toggleCategory(categoryFilter);
441
}
442
}));
443
}
444
}
445
}
446
447
private shouldShowLine(model: ITextModel, range: Range, positive: string[], negative: string[]): { show: boolean; matches: IModelDeltaDecoration[] } {
448
const matches: IModelDeltaDecoration[] = [];
449
450
// Check negative filters first - if any match, hide the line
451
if (negative.length > 0) {
452
for (const pattern of negative) {
453
const negativeMatches = model.findMatches(pattern, range, false, false, null, false);
454
if (negativeMatches.length > 0) {
455
return { show: false, matches: [] };
456
}
457
}
458
}
459
460
// If there are positive filters, at least one must match
461
if (positive.length > 0) {
462
let hasPositiveMatch = false;
463
for (const pattern of positive) {
464
const positiveMatches = model.findMatches(pattern, range, false, false, null, false);
465
if (positiveMatches.length > 0) {
466
hasPositiveMatch = true;
467
for (const match of positiveMatches) {
468
matches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
469
}
470
}
471
}
472
return { show: hasPositiveMatch, matches };
473
}
474
475
// No positive filters means show everything (that passed negative filters)
476
return { show: true, matches };
477
}
478
479
private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map<string, string> } {
480
const filters = this.outputService.filters;
481
const activeChannel = this.outputService.getActiveChannel();
482
const findMatches: IModelDeltaDecoration[] = [];
483
const hiddenAreas: Range[] = [];
484
const categories = new Map<string, string>();
485
486
const logEntries = activeChannel?.getLogEntries();
487
if (activeChannel && logEntries?.length) {
488
const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error;
489
490
const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber);
491
if (fromLogLevelEntryIndex === -1) {
492
return { findMatches, hiddenAreas, categories };
493
}
494
495
for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) {
496
const entry = logEntries[i];
497
if (entry.category) {
498
categories.set(`${activeChannel.id}:${entry.category}`, entry.category);
499
}
500
if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) {
501
hiddenAreas.push(entry.range);
502
continue;
503
}
504
if (!this.shouldShowCategory(activeChannel.id, entry, filters)) {
505
hiddenAreas.push(entry.range);
506
continue;
507
}
508
if (filters.includePatterns.length > 0 || filters.excludePatterns.length > 0) {
509
const result = this.shouldShowLine(model, entry.range, filters.includePatterns, filters.excludePatterns);
510
if (result.show) {
511
findMatches.push(...result.matches);
512
} else {
513
hiddenAreas.push(entry.range);
514
}
515
}
516
}
517
return { findMatches, hiddenAreas, categories };
518
}
519
520
if (filters.includePatterns.length === 0 && filters.excludePatterns.length === 0) {
521
return { findMatches, hiddenAreas, categories };
522
}
523
524
const lineCount = model.getLineCount();
525
for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) {
526
const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));
527
const result = this.shouldShowLine(model, lineRange, filters.includePatterns, filters.excludePatterns);
528
if (result.show) {
529
findMatches.push(...result.matches);
530
} else {
531
hiddenAreas.push(lineRange);
532
}
533
}
534
return { findMatches, hiddenAreas, categories };
535
}
536
537
private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean {
538
switch (entry.logLevel) {
539
case LogLevel.Trace:
540
return filters.trace;
541
case LogLevel.Debug:
542
return filters.debug;
543
case LogLevel.Info:
544
return filters.info;
545
case LogLevel.Warning:
546
return filters.warning;
547
case LogLevel.Error:
548
return filters.error;
549
}
550
return true;
551
}
552
553
private shouldShowCategory(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean {
554
if (!entry.category) {
555
return true;
556
}
557
return !filters.hasCategory(`${activeChannelId}:${entry.category}`);
558
}
559
}
560
561