Path: blob/main/src/vs/workbench/contrib/output/browser/outputServices.ts
5282 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*--------------------------------------------------------------------------------------------*/45import { Event, Emitter } from '../../../../base/common/event.js';6import { Schemas } from '../../../../base/common/network.js';7import { URI } from '../../../../base/common/uri.js';8import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';9import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';10import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';11import { Registry } from '../../../../platform/registry/common/platform.js';12import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_CATEGORY_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor, ILogEntry } from '../../../services/output/common/output.js';13import { OutputLinkProvider } from './outputLinkProvider.js';14import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js';15import { ITextModel } from '../../../../editor/common/model.js';16import { ILogService, ILoggerService, LogLevel, LogLevelToString } from '../../../../platform/log/common/log.js';17import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';18import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel, MultiFileOutputChannelModel } from '../common/outputChannelModel.js';19import { IViewsService } from '../../../services/views/common/viewsService.js';20import { OutputViewPane } from './outputView.js';21import { ILanguageService } from '../../../../editor/common/languages/language.js';22import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';23import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';24import { IFileService } from '../../../../platform/files/common/files.js';25import { localize } from '../../../../nls.js';26import { joinPath } from '../../../../base/common/resources.js';27import { VSBuffer } from '../../../../base/common/buffer.js';28import { telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js';29import { toLocalISOString } from '../../../../base/common/date.js';30import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';31import { IDefaultLogLevelsService } from '../../../services/log/common/defaultLogLevels.js';3233const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel';3435class OutputChannel extends Disposable implements IOutputChannel {3637scrollLock: boolean = false;38readonly model: IOutputChannelModel;39readonly id: string;40readonly label: string;41readonly uri: URI;4243constructor(44readonly outputChannelDescriptor: IOutputChannelDescriptor,45private readonly outputLocation: URI,46private readonly outputDirPromise: Promise<void>,47@ILanguageService private readonly languageService: ILanguageService,48@IInstantiationService private readonly instantiationService: IInstantiationService,49) {50super();51this.id = outputChannelDescriptor.id;52this.label = outputChannelDescriptor.label;53this.uri = URI.from({ scheme: Schemas.outputChannel, path: this.id });54this.model = this._register(this.createOutputChannelModel(this.uri, outputChannelDescriptor));55}5657private createOutputChannelModel(uri: URI, outputChannelDescriptor: IOutputChannelDescriptor): IOutputChannelModel {58const language = outputChannelDescriptor.languageId ? this.languageService.createById(outputChannelDescriptor.languageId) : this.languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME);59if (isMultiSourceOutputChannelDescriptor(outputChannelDescriptor)) {60return this.instantiationService.createInstance(MultiFileOutputChannelModel, uri, language, [...outputChannelDescriptor.source]);61}62if (isSingleSourceOutputChannelDescriptor(outputChannelDescriptor)) {63return this.instantiationService.createInstance(FileOutputChannelModel, uri, language, outputChannelDescriptor.source);64}65return this.instantiationService.createInstance(DelegatedOutputChannelModel, this.id, uri, language, this.outputLocation, this.outputDirPromise);66}6768getLogEntries(): ReadonlyArray<ILogEntry> {69return this.model.getLogEntries();70}7172append(output: string): void {73this.model.append(output);74}7576update(mode: OutputChannelUpdateMode, till?: number): void {77this.model.update(mode, till, true);78}7980clear(): void {81this.model.clear();82}8384replace(value: string): void {85this.model.replace(value);86}87}8889interface IOutputFilterOptions {90filterHistory: string[];91trace: boolean;92debug: boolean;93info: boolean;94warning: boolean;95error: boolean;96sources: string;97}9899class OutputViewFilters extends Disposable implements IOutputViewFilters {100101private readonly _onDidChange = this._register(new Emitter<void>());102readonly onDidChange = this._onDidChange.event;103104constructor(105options: IOutputFilterOptions,106private readonly contextKeyService: IContextKeyService107) {108super();109110this._trace = SHOW_TRACE_FILTER_CONTEXT.bindTo(this.contextKeyService);111this._trace.set(options.trace);112113this._debug = SHOW_DEBUG_FILTER_CONTEXT.bindTo(this.contextKeyService);114this._debug.set(options.debug);115116this._info = SHOW_INFO_FILTER_CONTEXT.bindTo(this.contextKeyService);117this._info.set(options.info);118119this._warning = SHOW_WARNING_FILTER_CONTEXT.bindTo(this.contextKeyService);120this._warning.set(options.warning);121122this._error = SHOW_ERROR_FILTER_CONTEXT.bindTo(this.contextKeyService);123this._error.set(options.error);124125this._categories = HIDE_CATEGORY_FILTER_CONTEXT.bindTo(this.contextKeyService);126this._categories.set(options.sources);127128this.filterHistory = options.filterHistory;129}130131filterHistory: string[];132133private _filterText = '';134private _includePatterns: string[] = [];135private _excludePatterns: string[] = [];136get text(): string {137return this._filterText;138}139set text(filterText: string) {140if (this._filterText !== filterText) {141this._filterText = filterText;142const { includePatterns, excludePatterns } = this.parseText(filterText);143this._includePatterns = includePatterns;144this._excludePatterns = excludePatterns;145this._onDidChange.fire();146}147}148private parseText(filterText: string): { includePatterns: string[]; excludePatterns: string[] } {149const includePatterns: string[] = [];150const excludePatterns: string[] = [];151152// Parse patterns respecting quoted strings153const patterns = this.splitByCommaRespectingQuotes(filterText);154155for (const pattern of patterns) {156const trimmed = pattern.trim();157if (trimmed.length === 0) {158continue;159}160161if (trimmed.startsWith('!')) {162// Negative filter - remove the ! prefix163const negativePattern = trimmed.substring(1).trim();164if (negativePattern.length > 0) {165excludePatterns.push(negativePattern);166}167} else {168includePatterns.push(trimmed);169}170}171172return { includePatterns, excludePatterns };173}174175get includePatterns(): string[] {176return this._includePatterns;177}178179get excludePatterns(): string[] {180return this._excludePatterns;181}182183private splitByCommaRespectingQuotes(text: string): string[] {184const patterns: string[] = [];185let current = '';186let inQuotes = false;187let quoteChar = '';188189for (let i = 0; i < text.length; i++) {190const char = text[i];191192if (!inQuotes && (char === '"')) {193// Start of quoted string194inQuotes = true;195quoteChar = char;196current += char;197} else if (inQuotes && char === quoteChar) {198// End of quoted string199inQuotes = false;200current += char;201} else if (!inQuotes && char === ',') {202// Comma outside quotes - split here203if (current.length > 0) {204patterns.push(current);205}206current = '';207} else {208current += char;209}210}211212// Add the last pattern213if (current.length > 0) {214patterns.push(current);215}216217return patterns;218}219220private readonly _trace: IContextKey<boolean>;221get trace(): boolean {222return !!this._trace.get();223}224set trace(trace: boolean) {225if (this._trace.get() !== trace) {226this._trace.set(trace);227this._onDidChange.fire();228}229}230231private readonly _debug: IContextKey<boolean>;232get debug(): boolean {233return !!this._debug.get();234}235set debug(debug: boolean) {236if (this._debug.get() !== debug) {237this._debug.set(debug);238this._onDidChange.fire();239}240}241242private readonly _info: IContextKey<boolean>;243get info(): boolean {244return !!this._info.get();245}246set info(info: boolean) {247if (this._info.get() !== info) {248this._info.set(info);249this._onDidChange.fire();250}251}252253private readonly _warning: IContextKey<boolean>;254get warning(): boolean {255return !!this._warning.get();256}257set warning(warning: boolean) {258if (this._warning.get() !== warning) {259this._warning.set(warning);260this._onDidChange.fire();261}262}263264private readonly _error: IContextKey<boolean>;265get error(): boolean {266return !!this._error.get();267}268set error(error: boolean) {269if (this._error.get() !== error) {270this._error.set(error);271this._onDidChange.fire();272}273}274275private readonly _categories: IContextKey<string>;276get categories(): string {277return this._categories.get() || ',';278}279set categories(categories: string) {280this._categories.set(categories);281this._onDidChange.fire();282}283284toggleCategory(category: string): void {285const categories = this.categories;286if (this.hasCategory(category)) {287this.categories = categories.replace(`,${category},`, ',');288} else {289this.categories = `${categories}${category},`;290}291}292293hasCategory(category: string): boolean {294if (category === ',') {295return false;296}297return this.categories.includes(`,${category},`);298}299}300301export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider {302303declare readonly _serviceBrand: undefined;304305private readonly channels = this._register(new DisposableMap<string, OutputChannel>());306private activeChannelIdInStorage: string;307private activeChannel?: OutputChannel;308309private readonly _onActiveOutputChannel = this._register(new Emitter<string>());310readonly onActiveOutputChannel: Event<string> = this._onActiveOutputChannel.event;311312private readonly activeOutputChannelContext: IContextKey<string>;313private readonly activeFileOutputChannelContext: IContextKey<boolean>;314private readonly activeLogOutputChannelContext: IContextKey<boolean>;315private readonly activeOutputChannelLevelSettableContext: IContextKey<boolean>;316private readonly activeOutputChannelLevelContext: IContextKey<string>;317private readonly activeOutputChannelLevelIsDefaultContext: IContextKey<boolean>;318319private readonly outputLocation: URI;320321readonly filters: OutputViewFilters;322323constructor(324@IStorageService private readonly storageService: IStorageService,325@IInstantiationService private readonly instantiationService: IInstantiationService,326@ITextModelService private readonly textModelService: ITextModelService,327@ILogService private readonly logService: ILogService,328@ILoggerService private readonly loggerService: ILoggerService,329@ILifecycleService private readonly lifecycleService: ILifecycleService,330@IViewsService private readonly viewsService: IViewsService,331@IContextKeyService contextKeyService: IContextKeyService,332@IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService,333@IFileDialogService private readonly fileDialogService: IFileDialogService,334@IFileService private readonly fileService: IFileService,335@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService336) {337super();338this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, '');339this.activeOutputChannelContext = ACTIVE_OUTPUT_CHANNEL_CONTEXT.bindTo(contextKeyService);340this.activeOutputChannelContext.set(this.activeChannelIdInStorage);341this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel)));342343this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService);344this.activeLogOutputChannelContext = CONTEXT_ACTIVE_LOG_FILE_OUTPUT.bindTo(contextKeyService);345this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService);346this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService);347this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService);348349this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`);350351// Register as text model content provider for output352this._register(textModelService.registerTextModelContentProvider(Schemas.outputChannel, this));353this._register(instantiationService.createInstance(OutputLinkProvider));354355// Create output channels for already registered channels356const registry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels);357for (const channelIdentifier of registry.getChannels()) {358this.onDidRegisterChannel(channelIdentifier.id);359}360this._register(registry.onDidRegisterChannel(id => this.onDidRegisterChannel(id)));361this._register(registry.onDidUpdateChannelSources(channel => this.onDidUpdateChannelSources(channel)));362this._register(registry.onDidRemoveChannel(channel => this.onDidRemoveChannel(channel)));363364// Set active channel to first channel if not set365if (!this.activeChannel) {366const channels = this.getChannelDescriptors();367this.setActiveChannel(channels && channels.length > 0 ? this.getChannel(channels[0].id) : undefined);368}369370this._register(Event.filter(this.viewsService.onDidChangeViewVisibility, e => e.id === OUTPUT_VIEW_ID && e.visible)(() => {371if (this.activeChannel) {372this.viewsService.getActiveViewWithId<OutputViewPane>(OUTPUT_VIEW_ID)?.showChannel(this.activeChannel, true);373}374}));375376this._register(this.loggerService.onDidChangeLogLevel(() => {377this.setLevelContext();378this.setLevelIsDefaultContext();379}));380this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => {381this.setLevelIsDefaultContext();382}));383384this._register(this.lifecycleService.onDidShutdown(() => this.dispose()));385386this.filters = this._register(new OutputViewFilters({387filterHistory: [],388trace: true,389debug: true,390info: true,391warning: true,392error: true,393sources: '',394}, contextKeyService));395}396397provideTextContent(resource: URI): Promise<ITextModel> | null {398const channel = <OutputChannel>this.getChannel(resource.path);399if (channel) {400return channel.model.loadModel();401}402return null;403}404405async showChannel(id: string, preserveFocus?: boolean): Promise<void> {406const channel = this.getChannel(id);407if (this.activeChannel?.id !== channel?.id) {408this.setActiveChannel(channel);409this._onActiveOutputChannel.fire(id);410}411const outputView = await this.viewsService.openView<OutputViewPane>(OUTPUT_VIEW_ID, !preserveFocus);412if (outputView && channel) {413outputView.showChannel(channel, !!preserveFocus);414}415}416417getChannel(id: string): OutputChannel | undefined {418return this.channels.get(id);419}420421getChannelDescriptor(id: string): IOutputChannelDescriptor | undefined {422return Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);423}424425getChannelDescriptors(): IOutputChannelDescriptor[] {426return Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannels();427}428429getActiveChannel(): IOutputChannel | undefined {430return this.activeChannel;431}432433canSetLogLevel(channel: IOutputChannelDescriptor): boolean {434return channel.log && channel.id !== telemetryLogId;435}436437getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined {438if (!channel.log) {439return undefined;440}441const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : [];442if (sources.length === 0) {443return undefined;444}445446const logLevel = this.loggerService.getLogLevel();447return sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), LogLevel.Error);448}449450setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void {451if (!channel.log) {452return;453}454const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : [];455if (sources.length === 0) {456return;457}458for (const source of sources) {459this.loggerService.setLogLevel(source.resource, logLevel);460}461}462463registerCompoundLogChannel(descriptors: IOutputChannelDescriptor[]): string {464const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels);465descriptors.sort((a, b) => a.label.localeCompare(b.label));466const id = descriptors.map(r => r.id.toLowerCase()).join('-');467if (!outputChannelRegistry.getChannel(id)) {468outputChannelRegistry.registerChannel({469id,470label: descriptors.map(r => r.label).join(', '),471log: descriptors.some(r => r.log),472user: true,473source: descriptors.map(descriptor => {474if (isSingleSourceOutputChannelDescriptor(descriptor)) {475return [{ resource: descriptor.source.resource, name: descriptor.source.name ?? descriptor.label }];476}477if (isMultiSourceOutputChannelDescriptor(descriptor)) {478return descriptor.source;479}480const channel = this.getChannel(descriptor.id);481if (channel) {482return channel.model.source;483}484return [];485}).flat(),486});487}488return id;489}490491async saveOutputAs(outputPath?: URI, ...channels: IOutputChannelDescriptor[]): Promise<void> {492let channel: IOutputChannel | undefined;493if (channels.length > 1) {494const compoundChannelId = this.registerCompoundLogChannel(channels);495channel = this.getChannel(compoundChannelId);496} else {497channel = this.getChannel(channels[0].id);498}499500if (!channel) {501return;502}503504try {505let uri: URI | undefined = outputPath;506if (!uri) {507const name = channels.length > 1 ? 'output' : channels[0].label;508uri = await this.fileDialogService.showSaveDialog({509title: localize('saveLog.dialogTitle', "Save Output As"),510availableFileSystems: [Schemas.file],511defaultUri: joinPath(await this.fileDialogService.defaultFilePath(), `${name}.log`),512filters: [{513name,514extensions: ['log']515}]516});517}518519if (!uri) {520return;521}522523const modelRef = await this.textModelService.createModelReference(channel.uri);524try {525await this.fileService.writeFile(uri, VSBuffer.fromString(modelRef.object.textEditorModel.getValue()));526} finally {527modelRef.dispose();528}529return;530}531finally {532if (channels.length > 1) {533Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(channel.id);534}535}536}537538private async onDidRegisterChannel(channelId: string): Promise<void> {539const channel = this.createChannel(channelId);540this.channels.set(channelId, channel);541if (!this.activeChannel || this.activeChannelIdInStorage === channelId) {542this.setActiveChannel(channel);543this._onActiveOutputChannel.fire(channelId);544const outputView = this.viewsService.getActiveViewWithId<OutputViewPane>(OUTPUT_VIEW_ID);545outputView?.showChannel(channel, true);546}547}548549private onDidUpdateChannelSources(channel: IMultiSourceOutputChannelDescriptor): void {550const outputChannel = this.channels.get(channel.id);551if (outputChannel) {552outputChannel.model.updateChannelSources(channel.source);553}554}555556private onDidRemoveChannel(channel: IOutputChannelDescriptor): void {557if (this.activeChannel?.id === channel.id) {558const channels = this.getChannelDescriptors();559if (channels[0]) {560this.showChannel(channels[0].id);561}562}563this.channels.deleteAndDispose(channel.id);564}565566private createChannel(id: string): OutputChannel {567const channel = this.instantiateChannel(id);568this._register(Event.once(channel.model.onDispose)(() => {569if (this.activeChannel === channel) {570const channels = this.getChannelDescriptors();571const channel = channels.length ? this.getChannel(channels[0].id) : undefined;572if (channel && this.viewsService.isViewVisible(OUTPUT_VIEW_ID)) {573this.showChannel(channel.id);574} else {575this.setActiveChannel(undefined);576}577}578Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(id);579}));580581return channel;582}583584private outputFolderCreationPromise: Promise<void> | null = null;585private instantiateChannel(id: string): OutputChannel {586const channelData = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);587if (!channelData) {588this.logService.error(`Channel '${id}' is not registered yet`);589throw new Error(`Channel '${id}' is not registered yet`);590}591if (!this.outputFolderCreationPromise) {592this.outputFolderCreationPromise = this.fileService.createFolder(this.outputLocation).then(() => undefined);593}594return this.instantiationService.createInstance(OutputChannel, channelData, this.outputLocation, this.outputFolderCreationPromise);595}596597private setLevelContext(): void {598const descriptor = this.activeChannel?.outputChannelDescriptor;599const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined;600this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : '');601}602603private async setLevelIsDefaultContext(): Promise<void> {604const descriptor = this.activeChannel?.outputChannelDescriptor;605const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined;606if (channelLogLevel !== undefined) {607const channelDefaultLogLevel = this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId);608this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel);609} else {610this.activeOutputChannelLevelIsDefaultContext.set(false);611}612}613614private setActiveChannel(channel: OutputChannel | undefined): void {615this.activeChannel = channel;616const descriptor = channel?.outputChannelDescriptor;617this.activeFileOutputChannelContext.set(!!descriptor && isSingleSourceOutputChannelDescriptor(descriptor));618this.activeLogOutputChannelContext.set(!!descriptor?.log);619this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && this.canSetLogLevel(descriptor));620this.setLevelIsDefaultContext();621this.setLevelContext();622623if (this.activeChannel) {624this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);625} else {626this.storageService.remove(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE);627}628}629}630631632