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