Path: blob/main/src/vs/workbench/contrib/output/browser/outputView.ts
5252 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 } 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';5152interface IOutputViewState {53filter?: string;54showTrace?: boolean;55showDebug?: boolean;56showInfo?: boolean;57showWarning?: boolean;58showError?: boolean;59categories?: string;60}6162export class OutputViewPane extends FilterViewPane {6364private readonly editor: OutputEditor;65private channelId: string | undefined;66private editorPromise: CancelablePromise<void> | null = null;6768private readonly scrollLockContextKey: IContextKey<boolean>;69get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); }70set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); }7172private readonly memento: Memento<IOutputViewState>;73private readonly panelState: IOutputViewState;7475constructor(76options: IViewPaneOptions,77@IKeybindingService keybindingService: IKeybindingService,78@IContextMenuService contextMenuService: IContextMenuService,79@IConfigurationService configurationService: IConfigurationService,80@IContextKeyService contextKeyService: IContextKeyService,81@IViewDescriptorService viewDescriptorService: IViewDescriptorService,82@IInstantiationService instantiationService: IInstantiationService,83@IOpenerService openerService: IOpenerService,84@IThemeService themeService: IThemeService,85@IHoverService hoverService: IHoverService,86@IOutputService private readonly outputService: IOutputService,87@IStorageService storageService: IStorageService,88) {89const memento = new Memento<IOutputViewState>(Markers.MARKERS_VIEW_STORAGE_ID, storageService);90const viewState = memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);91super({92...options,93filterOptions: {94placeholder: localize('outputView.filter.placeholder', "Filter (e.g. text, !excludeText, text1,text2)"),95focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key,96text: viewState.filter || '',97history: []98}99}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);100this.memento = memento;101this.panelState = viewState;102103const filters = outputService.filters;104filters.text = this.panelState.filter || '';105filters.trace = this.panelState.showTrace ?? true;106filters.debug = this.panelState.showDebug ?? true;107filters.info = this.panelState.showInfo ?? true;108filters.warning = this.panelState.showWarning ?? true;109filters.error = this.panelState.showError ?? true;110filters.categories = this.panelState.categories ?? '';111112this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService);113114const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));115this.editor = this._register(editorInstantiationService.createInstance(OutputEditor));116this._register(this.editor.onTitleAreaUpdate(() => {117this.updateTitle(this.editor.getTitle());118this.updateActions();119}));120this._register(this.onDidChangeBodyVisibility(() => this.onDidChangeVisibility(this.isBodyVisible())));121this._register(this.filterWidget.onDidChangeFilterText(text => outputService.filters.text = text));122123this.checkMoreFilters();124this._register(outputService.filters.onDidChange(() => this.checkMoreFilters()));125}126127showChannel(channel: IOutputChannel, preserveFocus: boolean): void {128if (this.channelId !== channel.id) {129this.setInput(channel);130}131if (!preserveFocus) {132this.focus();133}134}135136override focus(): void {137super.focus();138this.editorPromise?.then(() => this.editor.focus());139}140141public clearFilterText(): void {142this.filterWidget.setFilterText('');143}144145protected override renderBody(container: HTMLElement): void {146super.renderBody(container);147this.editor.create(container);148container.classList.add('output-view');149const codeEditor = <ICodeEditor>this.editor.getControl();150codeEditor.setAriaOptions({ role: 'document', activeDescendant: undefined });151this._register(codeEditor.onDidChangeModelContent(() => {152if (!this.scrollLock) {153this.editor.revealLastLine();154}155}));156this._register(codeEditor.onDidChangeCursorPosition((e) => {157if (e.reason !== CursorChangeReason.Explicit) {158return;159}160161if (!this.configurationService.getValue('output.smartScroll.enabled')) {162return;163}164165const model = codeEditor.getModel();166if (model) {167const newPositionLine = e.position.lineNumber;168const lastLine = model.getLineCount();169this.scrollLock = lastLine !== newPositionLine;170}171}));172}173174protected layoutBodyContent(height: number, width: number): void {175this.editor.layout(new Dimension(width, height));176}177178private onDidChangeVisibility(visible: boolean): void {179this.editor.setVisible(visible);180if (!visible) {181this.clearInput();182}183}184185private setInput(channel: IOutputChannel): void {186this.channelId = channel.id;187this.checkMoreFilters();188189const input = this.createInput(channel);190if (!this.editor.input || !input.matches(this.editor.input)) {191this.editorPromise?.cancel();192this.editorPromise = createCancelablePromise(token => this.editor.setInput(input, { preserveFocus: true }, Object.create(null), token));193}194195}196197private checkMoreFilters(): void {198const filters = this.outputService.filters;199this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.categories.includes(`,${this.channelId}:`)));200}201202private clearInput(): void {203this.channelId = undefined;204this.editor.clearInput();205this.editorPromise = null;206}207208private createInput(channel: IOutputChannel): TextResourceEditorInput {209return 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);210}211212override saveState(): void {213const filters = this.outputService.filters;214this.panelState.filter = filters.text;215this.panelState.showTrace = filters.trace;216this.panelState.showDebug = filters.debug;217this.panelState.showInfo = filters.info;218this.panelState.showWarning = filters.warning;219this.panelState.showError = filters.error;220this.panelState.categories = filters.categories;221222this.memento.saveMemento();223super.saveState();224}225226}227228export class OutputEditor extends AbstractTextResourceEditor {229private readonly resourceContext: ResourceContextKey;230231constructor(232@ITelemetryService telemetryService: ITelemetryService,233@IInstantiationService instantiationService: IInstantiationService,234@IStorageService storageService: IStorageService,235@IConfigurationService private readonly configurationService: IConfigurationService,236@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,237@IThemeService themeService: IThemeService,238@IEditorGroupsService editorGroupService: IEditorGroupsService,239@IEditorService editorService: IEditorService,240@IFileService fileService: IFileService241) {242super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* this is not correct but pragmatic */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService);243244this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));245}246247override getId(): string {248return OUTPUT_VIEW_ID;249}250251override getTitle(): string {252return nls.localize('output', "Output");253}254255protected override getConfigurationOverrides(configuration: IEditorConfiguration): ICodeEditorOptions {256const options = super.getConfigurationOverrides(configuration);257options.wordWrap = 'on'; // all output editors wrap258options.lineNumbers = 'off'; // all output editors hide line numbers259options.glyphMargin = false;260options.lineDecorationsWidth = 20;261options.rulers = [];262options.folding = false;263options.scrollBeyondLastLine = false;264options.renderLineHighlight = 'none';265options.minimap = { enabled: false };266options.renderValidationDecorations = 'editable';267options.padding = undefined;268options.readOnly = true;269options.domReadOnly = true;270options.roundedSelection = false;271options.unicodeHighlight = {272nonBasicASCII: false,273invisibleCharacters: false,274ambiguousCharacters: false,275};276277const outputConfig = this.configurationService.getValue<{ 'editor.minimap.enabled'?: boolean; 'editor.wordWrap'?: 'off' | 'on' | 'wordWrapColumn' | 'bounded' }>('[Log]');278if (outputConfig) {279if (outputConfig['editor.minimap.enabled']) {280options.minimap = { enabled: true };281}282if (outputConfig['editor.wordWrap']) {283options.wordWrap = outputConfig['editor.wordWrap'];284}285}286287return options;288}289290protected getAriaLabel(): string {291return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel");292}293294protected override computeAriaLabel(): string {295return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel();296}297298override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {299const focus = !(options && options.preserveFocus);300if (this.input && input.matches(this.input)) {301return;302}303304if (this.input) {305// Dispose previous input (Output panel is not a workbench editor)306this.input.dispose();307}308await super.setInput(input, options, context, token);309310this.resourceContext.set(input.resource);311312if (focus) {313this.focus();314}315this.revealLastLine();316}317318override clearInput(): void {319if (this.input) {320// Dispose current input (Output panel is not a workbench editor)321this.input.dispose();322}323super.clearInput();324325this.resourceContext.reset();326}327328protected override createEditor(parent: HTMLElement): void {329330parent.setAttribute('role', 'document');331332super.createEditor(parent);333334const scopedContextKeyService = this.scopedContextKeyService;335if (scopedContextKeyService) {336CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true);337}338}339340private _getContributions(): IEditorContributionDescription[] {341return [342...EditorExtensionsRegistry.getEditorContributions(),343{344id: FilterController.ID,345ctor: FilterController as EditorContributionCtor,346instantiation: EditorContributionInstantiation.Eager347}348];349}350351protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {352return { contributions: this._getContributions() };353}354355}356357export class FilterController extends Disposable implements IEditorContribution {358359public static readonly ID = 'output.editor.contrib.filterController';360361private readonly modelDisposables: DisposableStore = this._register(new DisposableStore());362private hiddenAreas: Range[] = [];363private readonly categories = new Map<string, string>();364private readonly decorationsCollection: IEditorDecorationsCollection;365366constructor(367private readonly editor: ICodeEditor,368@IOutputService private readonly outputService: IOutputService,369) {370super();371this.decorationsCollection = editor.createDecorationsCollection();372this._register(editor.onDidChangeModel(() => this.onDidChangeModel()));373this._register(this.outputService.filters.onDidChange(() => editor.hasModel() && this.filter(editor.getModel())));374}375376private onDidChangeModel(): void {377this.modelDisposables.clear();378this.hiddenAreas = [];379this.categories.clear();380381if (!this.editor.hasModel()) {382return;383}384385const model = this.editor.getModel();386this.filter(model);387388const computeEndLineNumber = () => {389const endLineNumber = model.getLineCount();390return endLineNumber > 1 && model.getLineMaxColumn(endLineNumber) === 1 ? endLineNumber - 1 : endLineNumber;391};392393let endLineNumber = computeEndLineNumber();394395this.modelDisposables.add(model.onDidChangeContent(e => {396if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) {397this.filterIncremental(model, endLineNumber + 1);398} else {399this.filter(model);400}401endLineNumber = computeEndLineNumber();402}));403}404405private filter(model: ITextModel): void {406this.hiddenAreas = [];407this.decorationsCollection.clear();408this.filterIncremental(model, 1);409}410411private filterIncremental(model: ITextModel, fromLineNumber: number): void {412const { findMatches, hiddenAreas, categories: sources } = this.compute(model, fromLineNumber);413this.hiddenAreas.push(...hiddenAreas);414this.editor.setHiddenAreas(this.hiddenAreas, this);415if (findMatches.length) {416this.decorationsCollection.append(findMatches);417}418if (sources.size) {419const that = this;420for (const [categoryFilter, categoryName] of sources) {421if (this.categories.has(categoryFilter)) {422continue;423}424this.categories.set(categoryFilter, categoryName);425this.modelDisposables.add(registerAction2(class extends Action2 {426constructor() {427super({428id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${categoryFilter}`,429title: categoryName,430toggled: ContextKeyExpr.regex(HIDE_CATEGORY_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(categoryFilter)},.*`)).negate(),431menu: {432id: viewFilterSubmenu,433group: '1_category_filter',434when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)),435}436});437}438async run(): Promise<void> {439that.outputService.filters.toggleCategory(categoryFilter);440}441}));442}443}444}445446private shouldShowLine(model: ITextModel, range: Range, positive: string[], negative: string[]): { show: boolean; matches: IModelDeltaDecoration[] } {447const matches: IModelDeltaDecoration[] = [];448449// Check negative filters first - if any match, hide the line450if (negative.length > 0) {451for (const pattern of negative) {452const negativeMatches = model.findMatches(pattern, range, false, false, null, false);453if (negativeMatches.length > 0) {454return { show: false, matches: [] };455}456}457}458459// If there are positive filters, at least one must match460if (positive.length > 0) {461let hasPositiveMatch = false;462for (const pattern of positive) {463const positiveMatches = model.findMatches(pattern, range, false, false, null, false);464if (positiveMatches.length > 0) {465hasPositiveMatch = true;466for (const match of positiveMatches) {467matches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });468}469}470}471return { show: hasPositiveMatch, matches };472}473474// No positive filters means show everything (that passed negative filters)475return { show: true, matches };476}477478private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map<string, string> } {479const filters = this.outputService.filters;480const activeChannel = this.outputService.getActiveChannel();481const findMatches: IModelDeltaDecoration[] = [];482const hiddenAreas: Range[] = [];483const categories = new Map<string, string>();484485const logEntries = activeChannel?.getLogEntries();486if (activeChannel && logEntries?.length) {487const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error;488489const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber);490if (fromLogLevelEntryIndex === -1) {491return { findMatches, hiddenAreas, categories };492}493494for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) {495const entry = logEntries[i];496if (entry.category) {497categories.set(`${activeChannel.id}:${entry.category}`, entry.category);498}499if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) {500hiddenAreas.push(entry.range);501continue;502}503if (!this.shouldShowCategory(activeChannel.id, entry, filters)) {504hiddenAreas.push(entry.range);505continue;506}507if (filters.includePatterns.length > 0 || filters.excludePatterns.length > 0) {508const result = this.shouldShowLine(model, entry.range, filters.includePatterns, filters.excludePatterns);509if (result.show) {510findMatches.push(...result.matches);511} else {512hiddenAreas.push(entry.range);513}514}515}516return { findMatches, hiddenAreas, categories };517}518519if (filters.includePatterns.length === 0 && filters.excludePatterns.length === 0) {520return { findMatches, hiddenAreas, categories };521}522523const lineCount = model.getLineCount();524for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) {525const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));526const result = this.shouldShowLine(model, lineRange, filters.includePatterns, filters.excludePatterns);527if (result.show) {528findMatches.push(...result.matches);529} else {530hiddenAreas.push(lineRange);531}532}533return { findMatches, hiddenAreas, categories };534}535536private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean {537switch (entry.logLevel) {538case LogLevel.Trace:539return filters.trace;540case LogLevel.Debug:541return filters.debug;542case LogLevel.Info:543return filters.info;544case LogLevel.Warning:545return filters.warning;546case LogLevel.Error:547return filters.error;548}549return true;550}551552private shouldShowCategory(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean {553if (!entry.category) {554return true;555}556return !filters.hasCategory(`${activeChannelId}:${entry.category}`);557}558}559560561