Path: blob/main/src/vs/workbench/contrib/output/browser/outputView.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import './output.css';5import * as nls from '../../../../nls.js';6import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';7import { IEditorOptions as ICodeEditorOptions } from '../../../../editor/common/config/editorOptions.js';8import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';9import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';10import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import { IContextKeyService, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';13import { IEditorOpenContext } from '../../../common/editor.js';14import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js';15import { 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';16import { IThemeService } from '../../../../platform/theme/common/themeService.js';17import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';18import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';19import { CancellationToken } from '../../../../base/common/cancellation.js';20import { IEditorService } from '../../../services/editor/common/editorService.js';21import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';22import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js';23import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';24import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';25import { IViewDescriptorService } from '../../../common/views.js';26import { TextResourceEditorInput } from '../../../common/editor/textResourceEditorInput.js';27import { IOpenerService } from '../../../../platform/opener/common/opener.js';28import { Dimension } from '../../../../base/browser/dom.js';29import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';30import { CancelablePromise, createCancelablePromise } from '../../../../base/common/async.js';31import { IFileService } from '../../../../platform/files/common/files.js';32import { ResourceContextKey } from '../../../common/contextkeys.js';33import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';34import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js';35import { computeEditorAriaLabel } from '../../../browser/editor.js';36import { IHoverService } from '../../../../platform/hover/browser/hover.js';37import { localize } from '../../../../nls.js';38import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';39import { LogLevel } from '../../../../platform/log/common/log.js';40import { IEditorContributionDescription, EditorExtensionsRegistry, EditorContributionInstantiation, EditorContributionCtor } from '../../../../editor/browser/editorExtensions.js';41import { ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';42import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';43import { IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js';44import { Range } from '../../../../editor/common/core/range.js';45import { FindDecorations } from '../../../../editor/contrib/find/browser/findDecorations.js';46import { Memento, MementoObject } from '../../../common/memento.js';47import { Markers } from '../../markers/common/markers.js';48import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';49import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js';50import { escapeRegExpCharacters } from '../../../../base/common/strings.js';5152export class OutputViewPane extends FilterViewPane {5354private readonly editor: OutputEditor;55private channelId: string | undefined;56private editorPromise: CancelablePromise<void> | null = null;5758private readonly scrollLockContextKey: IContextKey<boolean>;59get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); }60set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); }6162private readonly memento: Memento;63private readonly panelState: MementoObject;6465constructor(66options: IViewPaneOptions,67@IKeybindingService keybindingService: IKeybindingService,68@IContextMenuService contextMenuService: IContextMenuService,69@IConfigurationService configurationService: IConfigurationService,70@IContextKeyService contextKeyService: IContextKeyService,71@IViewDescriptorService viewDescriptorService: IViewDescriptorService,72@IInstantiationService instantiationService: IInstantiationService,73@IOpenerService openerService: IOpenerService,74@IThemeService themeService: IThemeService,75@IHoverService hoverService: IHoverService,76@IOutputService private readonly outputService: IOutputService,77@IStorageService storageService: IStorageService,78) {79const memento = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService);80const viewState = memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);81super({82...options,83filterOptions: {84placeholder: localize('outputView.filter.placeholder', "Filter"),85focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key,86text: viewState['filter'] || '',87history: []88}89}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);90this.memento = memento;91this.panelState = viewState;9293const filters = outputService.filters;94filters.text = this.panelState['filter'] || '';95filters.trace = this.panelState['showTrace'] ?? true;96filters.debug = this.panelState['showDebug'] ?? true;97filters.info = this.panelState['showInfo'] ?? true;98filters.warning = this.panelState['showWarning'] ?? true;99filters.error = this.panelState['showError'] ?? true;100filters.categories = this.panelState['categories'] ?? '';101102this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService);103104const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));105this.editor = this._register(editorInstantiationService.createInstance(OutputEditor));106this._register(this.editor.onTitleAreaUpdate(() => {107this.updateTitle(this.editor.getTitle());108this.updateActions();109}));110this._register(this.onDidChangeBodyVisibility(() => this.onDidChangeVisibility(this.isBodyVisible())));111this._register(this.filterWidget.onDidChangeFilterText(text => outputService.filters.text = text));112113this.checkMoreFilters();114this._register(outputService.filters.onDidChange(() => this.checkMoreFilters()));115}116117showChannel(channel: IOutputChannel, preserveFocus: boolean): void {118if (this.channelId !== channel.id) {119this.setInput(channel);120}121if (!preserveFocus) {122this.focus();123}124}125126override focus(): void {127super.focus();128this.editorPromise?.then(() => this.editor.focus());129}130131public clearFilterText(): void {132this.filterWidget.setFilterText('');133}134135protected override renderBody(container: HTMLElement): void {136super.renderBody(container);137this.editor.create(container);138container.classList.add('output-view');139const codeEditor = <ICodeEditor>this.editor.getControl();140codeEditor.setAriaOptions({ role: 'document', activeDescendant: undefined });141this._register(codeEditor.onDidChangeModelContent(() => {142if (!this.scrollLock) {143this.editor.revealLastLine();144}145}));146this._register(codeEditor.onDidChangeCursorPosition((e) => {147if (e.reason !== CursorChangeReason.Explicit) {148return;149}150151if (!this.configurationService.getValue('output.smartScroll.enabled')) {152return;153}154155const model = codeEditor.getModel();156if (model) {157const newPositionLine = e.position.lineNumber;158const lastLine = model.getLineCount();159this.scrollLock = lastLine !== newPositionLine;160}161}));162}163164protected layoutBodyContent(height: number, width: number): void {165this.editor.layout(new Dimension(width, height));166}167168private onDidChangeVisibility(visible: boolean): void {169this.editor.setVisible(visible);170if (!visible) {171this.clearInput();172}173}174175private setInput(channel: IOutputChannel): void {176this.channelId = channel.id;177this.checkMoreFilters();178179const input = this.createInput(channel);180if (!this.editor.input || !input.matches(this.editor.input)) {181this.editorPromise?.cancel();182this.editorPromise = createCancelablePromise(token => this.editor.setInput(this.createInput(channel), { preserveFocus: true }, Object.create(null), token));183}184185}186187private checkMoreFilters(): void {188const filters = this.outputService.filters;189this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.categories.includes(`,${this.channelId}:`)));190}191192private clearInput(): void {193this.channelId = undefined;194this.editor.clearInput();195this.editorPromise = null;196}197198private createInput(channel: IOutputChannel): TextResourceEditorInput {199return 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);200}201202override saveState(): void {203const filters = this.outputService.filters;204this.panelState['filter'] = filters.text;205this.panelState['showTrace'] = filters.trace;206this.panelState['showDebug'] = filters.debug;207this.panelState['showInfo'] = filters.info;208this.panelState['showWarning'] = filters.warning;209this.panelState['showError'] = filters.error;210this.panelState['categories'] = filters.categories;211212this.memento.saveMemento();213super.saveState();214}215216}217218export class OutputEditor extends AbstractTextResourceEditor {219private readonly resourceContext: ResourceContextKey;220221constructor(222@ITelemetryService telemetryService: ITelemetryService,223@IInstantiationService instantiationService: IInstantiationService,224@IStorageService storageService: IStorageService,225@IConfigurationService private readonly configurationService: IConfigurationService,226@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,227@IThemeService themeService: IThemeService,228@IEditorGroupsService editorGroupService: IEditorGroupsService,229@IEditorService editorService: IEditorService,230@IFileService fileService: IFileService231) {232super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* this is not correct but pragmatic */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService);233234this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));235}236237override getId(): string {238return OUTPUT_VIEW_ID;239}240241override getTitle(): string {242return nls.localize('output', "Output");243}244245protected override getConfigurationOverrides(configuration: IEditorConfiguration): ICodeEditorOptions {246const options = super.getConfigurationOverrides(configuration);247options.wordWrap = 'on'; // all output editors wrap248options.lineNumbers = 'off'; // all output editors hide line numbers249options.glyphMargin = false;250options.lineDecorationsWidth = 20;251options.rulers = [];252options.folding = false;253options.scrollBeyondLastLine = false;254options.renderLineHighlight = 'none';255options.minimap = { enabled: false };256options.renderValidationDecorations = 'editable';257options.padding = undefined;258options.readOnly = true;259options.domReadOnly = true;260options.unicodeHighlight = {261nonBasicASCII: false,262invisibleCharacters: false,263ambiguousCharacters: false,264};265266const outputConfig = this.configurationService.getValue<any>('[Log]');267if (outputConfig) {268if (outputConfig['editor.minimap.enabled']) {269options.minimap = { enabled: true };270}271if ('editor.wordWrap' in outputConfig) {272options.wordWrap = outputConfig['editor.wordWrap'];273}274}275276return options;277}278279protected getAriaLabel(): string {280return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel");281}282283protected override computeAriaLabel(): string {284return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel();285}286287override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {288const focus = !(options && options.preserveFocus);289if (this.input && input.matches(this.input)) {290return;291}292293if (this.input) {294// Dispose previous input (Output panel is not a workbench editor)295this.input.dispose();296}297await super.setInput(input, options, context, token);298299this.resourceContext.set(input.resource);300301if (focus) {302this.focus();303}304this.revealLastLine();305}306307override clearInput(): void {308if (this.input) {309// Dispose current input (Output panel is not a workbench editor)310this.input.dispose();311}312super.clearInput();313314this.resourceContext.reset();315}316317protected override createEditor(parent: HTMLElement): void {318319parent.setAttribute('role', 'document');320321super.createEditor(parent);322323const scopedContextKeyService = this.scopedContextKeyService;324if (scopedContextKeyService) {325CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true);326}327}328329private _getContributions(): IEditorContributionDescription[] {330return [331...EditorExtensionsRegistry.getEditorContributions(),332{333id: FilterController.ID,334ctor: FilterController as EditorContributionCtor,335instantiation: EditorContributionInstantiation.Eager336}337];338}339340protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {341return { contributions: this._getContributions() };342}343344}345346export class FilterController extends Disposable implements IEditorContribution {347348public static readonly ID = 'output.editor.contrib.filterController';349350private readonly modelDisposables: DisposableStore = this._register(new DisposableStore());351private hiddenAreas: Range[] = [];352private readonly categories = new Map<string, string>();353private readonly decorationsCollection: IEditorDecorationsCollection;354355constructor(356private readonly editor: ICodeEditor,357@IOutputService private readonly outputService: IOutputService,358) {359super();360this.decorationsCollection = editor.createDecorationsCollection();361this._register(editor.onDidChangeModel(() => this.onDidChangeModel()));362this._register(this.outputService.filters.onDidChange(() => editor.hasModel() && this.filter(editor.getModel())));363}364365private onDidChangeModel(): void {366this.modelDisposables.clear();367this.hiddenAreas = [];368this.categories.clear();369370if (!this.editor.hasModel()) {371return;372}373374const model = this.editor.getModel();375this.filter(model);376377const computeEndLineNumber = () => {378const endLineNumber = model.getLineCount();379return endLineNumber > 1 && model.getLineMaxColumn(endLineNumber) === 1 ? endLineNumber - 1 : endLineNumber;380};381382let endLineNumber = computeEndLineNumber();383384this.modelDisposables.add(model.onDidChangeContent(e => {385if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) {386this.filterIncremental(model, endLineNumber + 1);387} else {388this.filter(model);389}390endLineNumber = computeEndLineNumber();391}));392}393394private filter(model: ITextModel): void {395this.hiddenAreas = [];396this.decorationsCollection.clear();397this.filterIncremental(model, 1);398}399400private filterIncremental(model: ITextModel, fromLineNumber: number): void {401const { findMatches, hiddenAreas, categories: sources } = this.compute(model, fromLineNumber);402this.hiddenAreas.push(...hiddenAreas);403this.editor.setHiddenAreas(this.hiddenAreas, this);404if (findMatches.length) {405this.decorationsCollection.append(findMatches);406}407if (sources.size) {408const that = this;409for (const [categoryFilter, categoryName] of sources) {410if (this.categories.has(categoryFilter)) {411continue;412}413this.categories.set(categoryFilter, categoryName);414this.modelDisposables.add(registerAction2(class extends Action2 {415constructor() {416super({417id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${categoryFilter}`,418title: categoryName,419toggled: ContextKeyExpr.regex(HIDE_CATEGORY_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(categoryFilter)},.*`)).negate(),420menu: {421id: viewFilterSubmenu,422group: '1_category_filter',423when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)),424}425});426}427async run(): Promise<void> {428that.outputService.filters.toggleCategory(categoryFilter);429}430}));431}432}433}434435private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map<string, string> } {436const filters = this.outputService.filters;437const activeChannel = this.outputService.getActiveChannel();438const findMatches: IModelDeltaDecoration[] = [];439const hiddenAreas: Range[] = [];440const categories = new Map<string, string>();441442const logEntries = activeChannel?.getLogEntries();443if (activeChannel && logEntries?.length) {444const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error;445446const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber);447if (fromLogLevelEntryIndex === -1) {448return { findMatches, hiddenAreas, categories };449}450451for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) {452const entry = logEntries[i];453if (entry.category) {454categories.set(`${activeChannel.id}:${entry.category}`, entry.category);455}456if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) {457hiddenAreas.push(entry.range);458continue;459}460if (!this.shouldShowCategory(activeChannel.id, entry, filters)) {461hiddenAreas.push(entry.range);462continue;463}464if (filters.text) {465const matches = model.findMatches(filters.text, entry.range, false, false, null, false);466if (matches.length) {467for (const match of matches) {468findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });469}470} else {471hiddenAreas.push(entry.range);472}473}474}475return { findMatches, hiddenAreas, categories };476}477478if (!filters.text) {479return { findMatches, hiddenAreas, categories };480}481482const lineCount = model.getLineCount();483for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) {484const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));485const matches = model.findMatches(filters.text, lineRange, false, false, null, false);486if (matches.length) {487for (const match of matches) {488findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });489}490} else {491hiddenAreas.push(lineRange);492}493}494return { findMatches, hiddenAreas, categories };495}496497private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean {498switch (entry.logLevel) {499case LogLevel.Trace:500return filters.trace;501case LogLevel.Debug:502return filters.debug;503case LogLevel.Info:504return filters.info;505case LogLevel.Warning:506return filters.warning;507case LogLevel.Error:508return filters.error;509}510return true;511}512513private shouldShowCategory(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean {514if (!entry.category) {515return true;516}517return !filters.hasCategory(`${activeChannelId}:${entry.category}`);518}519}520521522