Path: blob/main/src/vs/workbench/contrib/output/browser/outputServices.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*--------------------------------------------------------------------------------------------*/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 { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js';24import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';25import { IFileService } from '../../../../platform/files/common/files.js';26import { localize } from '../../../../nls.js';27import { joinPath } from '../../../../base/common/resources.js';28import { VSBuffer } from '../../../../base/common/buffer.js';29import { telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js';30import { toLocalISOString } from '../../../../base/common/date.js';31import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.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 = '';134get text(): string {135return this._filterText;136}137set text(filterText: string) {138if (this._filterText !== filterText) {139this._filterText = filterText;140this._onDidChange.fire();141}142}143144private readonly _trace: IContextKey<boolean>;145get trace(): boolean {146return !!this._trace.get();147}148set trace(trace: boolean) {149if (this._trace.get() !== trace) {150this._trace.set(trace);151this._onDidChange.fire();152}153}154155private readonly _debug: IContextKey<boolean>;156get debug(): boolean {157return !!this._debug.get();158}159set debug(debug: boolean) {160if (this._debug.get() !== debug) {161this._debug.set(debug);162this._onDidChange.fire();163}164}165166private readonly _info: IContextKey<boolean>;167get info(): boolean {168return !!this._info.get();169}170set info(info: boolean) {171if (this._info.get() !== info) {172this._info.set(info);173this._onDidChange.fire();174}175}176177private readonly _warning: IContextKey<boolean>;178get warning(): boolean {179return !!this._warning.get();180}181set warning(warning: boolean) {182if (this._warning.get() !== warning) {183this._warning.set(warning);184this._onDidChange.fire();185}186}187188private readonly _error: IContextKey<boolean>;189get error(): boolean {190return !!this._error.get();191}192set error(error: boolean) {193if (this._error.get() !== error) {194this._error.set(error);195this._onDidChange.fire();196}197}198199private readonly _categories: IContextKey<string>;200get categories(): string {201return this._categories.get() || ',';202}203set categories(categories: string) {204this._categories.set(categories);205this._onDidChange.fire();206}207208toggleCategory(category: string): void {209const categories = this.categories;210if (this.hasCategory(category)) {211this.categories = categories.replace(`,${category},`, ',');212} else {213this.categories = `${categories}${category},`;214}215}216217hasCategory(category: string): boolean {218if (category === ',') {219return false;220}221return this.categories.includes(`,${category},`);222}223}224225export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider {226227declare readonly _serviceBrand: undefined;228229private readonly channels = this._register(new DisposableMap<string, OutputChannel>());230private activeChannelIdInStorage: string;231private activeChannel?: OutputChannel;232233private readonly _onActiveOutputChannel = this._register(new Emitter<string>());234readonly onActiveOutputChannel: Event<string> = this._onActiveOutputChannel.event;235236private readonly activeOutputChannelContext: IContextKey<string>;237private readonly activeFileOutputChannelContext: IContextKey<boolean>;238private readonly activeLogOutputChannelContext: IContextKey<boolean>;239private readonly activeOutputChannelLevelSettableContext: IContextKey<boolean>;240private readonly activeOutputChannelLevelContext: IContextKey<string>;241private readonly activeOutputChannelLevelIsDefaultContext: IContextKey<boolean>;242243private readonly outputLocation: URI;244245readonly filters: OutputViewFilters;246247constructor(248@IStorageService private readonly storageService: IStorageService,249@IInstantiationService private readonly instantiationService: IInstantiationService,250@ITextModelService private readonly textModelService: ITextModelService,251@ILogService private readonly logService: ILogService,252@ILoggerService private readonly loggerService: ILoggerService,253@ILifecycleService private readonly lifecycleService: ILifecycleService,254@IViewsService private readonly viewsService: IViewsService,255@IContextKeyService contextKeyService: IContextKeyService,256@IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService,257@IFileDialogService private readonly fileDialogService: IFileDialogService,258@IFileService private readonly fileService: IFileService,259@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService260) {261super();262this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, '');263this.activeOutputChannelContext = ACTIVE_OUTPUT_CHANNEL_CONTEXT.bindTo(contextKeyService);264this.activeOutputChannelContext.set(this.activeChannelIdInStorage);265this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel)));266267this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService);268this.activeLogOutputChannelContext = CONTEXT_ACTIVE_LOG_FILE_OUTPUT.bindTo(contextKeyService);269this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService);270this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService);271this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService);272273this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`);274275// Register as text model content provider for output276this._register(textModelService.registerTextModelContentProvider(Schemas.outputChannel, this));277this._register(instantiationService.createInstance(OutputLinkProvider));278279// Create output channels for already registered channels280const registry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels);281for (const channelIdentifier of registry.getChannels()) {282this.onDidRegisterChannel(channelIdentifier.id);283}284this._register(registry.onDidRegisterChannel(id => this.onDidRegisterChannel(id)));285this._register(registry.onDidUpdateChannelSources(channel => this.onDidUpdateChannelSources(channel)));286this._register(registry.onDidRemoveChannel(channel => this.onDidRemoveChannel(channel)));287288// Set active channel to first channel if not set289if (!this.activeChannel) {290const channels = this.getChannelDescriptors();291this.setActiveChannel(channels && channels.length > 0 ? this.getChannel(channels[0].id) : undefined);292}293294this._register(Event.filter(this.viewsService.onDidChangeViewVisibility, e => e.id === OUTPUT_VIEW_ID && e.visible)(() => {295if (this.activeChannel) {296this.viewsService.getActiveViewWithId<OutputViewPane>(OUTPUT_VIEW_ID)?.showChannel(this.activeChannel, true);297}298}));299300this._register(this.loggerService.onDidChangeLogLevel(() => {301this.resetLogLevelFilters();302this.setLevelContext();303this.setLevelIsDefaultContext();304}));305this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => {306this.setLevelIsDefaultContext();307}));308309this._register(this.lifecycleService.onDidShutdown(() => this.dispose()));310311this.filters = this._register(new OutputViewFilters({312filterHistory: [],313trace: true,314debug: true,315info: true,316warning: true,317error: true,318sources: '',319}, contextKeyService));320}321322provideTextContent(resource: URI): Promise<ITextModel> | null {323const channel = <OutputChannel>this.getChannel(resource.path);324if (channel) {325return channel.model.loadModel();326}327return null;328}329330async showChannel(id: string, preserveFocus?: boolean): Promise<void> {331const channel = this.getChannel(id);332if (this.activeChannel?.id !== channel?.id) {333this.setActiveChannel(channel);334this._onActiveOutputChannel.fire(id);335}336const outputView = await this.viewsService.openView<OutputViewPane>(OUTPUT_VIEW_ID, !preserveFocus);337if (outputView && channel) {338outputView.showChannel(channel, !!preserveFocus);339}340}341342getChannel(id: string): OutputChannel | undefined {343return this.channels.get(id);344}345346getChannelDescriptor(id: string): IOutputChannelDescriptor | undefined {347return Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);348}349350getChannelDescriptors(): IOutputChannelDescriptor[] {351return Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannels();352}353354getActiveChannel(): IOutputChannel | undefined {355return this.activeChannel;356}357358canSetLogLevel(channel: IOutputChannelDescriptor): boolean {359return channel.log && channel.id !== telemetryLogId;360}361362getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined {363if (!channel.log) {364return undefined;365}366const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : [];367if (sources.length === 0) {368return undefined;369}370371const logLevel = this.loggerService.getLogLevel();372return sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), LogLevel.Error);373}374375setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void {376if (!channel.log) {377return;378}379const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : [];380if (sources.length === 0) {381return;382}383for (const source of sources) {384this.loggerService.setLogLevel(source.resource, logLevel);385}386}387388registerCompoundLogChannel(descriptors: IOutputChannelDescriptor[]): string {389const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels);390descriptors.sort((a, b) => a.label.localeCompare(b.label));391const id = descriptors.map(r => r.id.toLowerCase()).join('-');392if (!outputChannelRegistry.getChannel(id)) {393outputChannelRegistry.registerChannel({394id,395label: descriptors.map(r => r.label).join(', '),396log: descriptors.some(r => r.log),397user: true,398source: descriptors.map(descriptor => {399if (isSingleSourceOutputChannelDescriptor(descriptor)) {400return [{ resource: descriptor.source.resource, name: descriptor.source.name ?? descriptor.label }];401}402if (isMultiSourceOutputChannelDescriptor(descriptor)) {403return descriptor.source;404}405const channel = this.getChannel(descriptor.id);406if (channel) {407return channel.model.source;408}409return [];410}).flat(),411});412}413return id;414}415416async saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise<void> {417let channel: IOutputChannel | undefined;418if (channels.length > 1) {419const compoundChannelId = this.registerCompoundLogChannel(channels);420channel = this.getChannel(compoundChannelId);421} else {422channel = this.getChannel(channels[0].id);423}424425if (!channel) {426return;427}428429try {430const name = channels.length > 1 ? 'output' : channels[0].label;431const uri = await this.fileDialogService.showSaveDialog({432title: localize('saveLog.dialogTitle', "Save Output As"),433availableFileSystems: [Schemas.file],434defaultUri: joinPath(await this.fileDialogService.defaultFilePath(), `${name}.log`),435filters: [{436name,437extensions: ['log']438}]439});440441if (!uri) {442return;443}444445const modelRef = await this.textModelService.createModelReference(channel.uri);446try {447await this.fileService.writeFile(uri, VSBuffer.fromString(modelRef.object.textEditorModel.getValue()));448} finally {449modelRef.dispose();450}451return;452}453finally {454if (channels.length > 1) {455Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(channel.id);456}457}458}459460private async onDidRegisterChannel(channelId: string): Promise<void> {461const channel = this.createChannel(channelId);462this.channels.set(channelId, channel);463if (!this.activeChannel || this.activeChannelIdInStorage === channelId) {464this.setActiveChannel(channel);465this._onActiveOutputChannel.fire(channelId);466const outputView = this.viewsService.getActiveViewWithId<OutputViewPane>(OUTPUT_VIEW_ID);467outputView?.showChannel(channel, true);468}469}470471private onDidUpdateChannelSources(channel: IMultiSourceOutputChannelDescriptor): void {472const outputChannel = this.channels.get(channel.id);473if (outputChannel) {474outputChannel.model.updateChannelSources(channel.source);475}476}477478private onDidRemoveChannel(channel: IOutputChannelDescriptor): void {479if (this.activeChannel?.id === channel.id) {480const channels = this.getChannelDescriptors();481if (channels[0]) {482this.showChannel(channels[0].id);483}484}485this.channels.deleteAndDispose(channel.id);486}487488private createChannel(id: string): OutputChannel {489const channel = this.instantiateChannel(id);490this._register(Event.once(channel.model.onDispose)(() => {491if (this.activeChannel === channel) {492const channels = this.getChannelDescriptors();493const channel = channels.length ? this.getChannel(channels[0].id) : undefined;494if (channel && this.viewsService.isViewVisible(OUTPUT_VIEW_ID)) {495this.showChannel(channel.id);496} else {497this.setActiveChannel(undefined);498}499}500Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(id);501}));502503return channel;504}505506private outputFolderCreationPromise: Promise<void> | null = null;507private instantiateChannel(id: string): OutputChannel {508const channelData = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);509if (!channelData) {510this.logService.error(`Channel '${id}' is not registered yet`);511throw new Error(`Channel '${id}' is not registered yet`);512}513if (!this.outputFolderCreationPromise) {514this.outputFolderCreationPromise = this.fileService.createFolder(this.outputLocation).then(() => undefined);515}516return this.instantiationService.createInstance(OutputChannel, channelData, this.outputLocation, this.outputFolderCreationPromise);517}518519private resetLogLevelFilters(): void {520const descriptor = this.activeChannel?.outputChannelDescriptor;521const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined;522if (channelLogLevel !== undefined) {523this.filters.error = channelLogLevel <= LogLevel.Error;524this.filters.warning = channelLogLevel <= LogLevel.Warning;525this.filters.info = channelLogLevel <= LogLevel.Info;526this.filters.debug = channelLogLevel <= LogLevel.Debug;527this.filters.trace = channelLogLevel <= LogLevel.Trace;528}529}530531private setLevelContext(): void {532const descriptor = this.activeChannel?.outputChannelDescriptor;533const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined;534this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : '');535}536537private async setLevelIsDefaultContext(): Promise<void> {538const descriptor = this.activeChannel?.outputChannelDescriptor;539const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined;540if (channelLogLevel !== undefined) {541const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId);542this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel);543} else {544this.activeOutputChannelLevelIsDefaultContext.set(false);545}546}547548private setActiveChannel(channel: OutputChannel | undefined): void {549this.activeChannel = channel;550const descriptor = channel?.outputChannelDescriptor;551this.activeFileOutputChannelContext.set(!!descriptor && isSingleSourceOutputChannelDescriptor(descriptor));552this.activeLogOutputChannelContext.set(!!descriptor?.log);553this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && this.canSetLogLevel(descriptor));554this.setLevelIsDefaultContext();555this.setLevelContext();556557if (this.activeChannel) {558this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);559} else {560this.storageService.remove(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE);561}562}563}564565566