UNPKG

@theia/output

Version:
327 lines • 15.5 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2018 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.OutputChannel = exports.OutputChannelSeverity = exports.OutputChannelManager = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("@theia/core/shared/inversify"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const core_1 = require("@theia/core"); const monaco_text_model_service_1 = require("@theia/monaco/lib/browser/monaco-text-model-service"); const output_uri_1 = require("../common/output-uri"); const output_resource_1 = require("../browser/output-resource"); const output_preferences_1 = require("../common/output-preferences"); const monaco = tslib_1.__importStar(require("@theia/monaco-editor-core")); const p_queue_1 = tslib_1.__importDefault(require("p-queue")); let OutputChannelManager = class OutputChannelManager { constructor() { this.channels = new Map(); this.resources = new Map(); this.channelAddedEmitter = new core_1.Emitter(); this.channelDeletedEmitter = new core_1.Emitter(); this.channelWasShownEmitter = new core_1.Emitter(); this.channelWasHiddenEmitter = new core_1.Emitter(); this.selectedChannelChangedEmitter = new core_1.Emitter(); this.onChannelAdded = this.channelAddedEmitter.event; this.onChannelDeleted = this.channelDeletedEmitter.event; this.onChannelWasShown = this.channelWasShownEmitter.event; this.onChannelWasHidden = this.channelWasHiddenEmitter.event; this.onSelectedChannelChanged = this.selectedChannelChangedEmitter.event; this.toDispose = new core_1.DisposableCollection(); this.toDisposeOnChannelDeletion = new Map(); } getChannel(name) { const existing = this.channels.get(name); if (existing) { return existing; } // We have to register the resource first, because `textModelService#createModelReference` will require it // right after creating the monaco.editor.ITextModel. // All `append` and `appendLine` will be deferred until the underlying text-model instantiation. let resource = this.resources.get(name); if (!resource) { const uri = output_uri_1.OutputUri.create(name); const editorModelRef = new promise_util_1.Deferred(); resource = this.createResource({ uri, editorModelRef }); this.resources.set(name, resource); this.textModelService.createModelReference(uri).then(ref => editorModelRef.resolve(ref)); } const channel = this.createChannel(resource); this.channels.set(name, channel); this.toDisposeOnChannelDeletion.set(name, this.registerListeners(channel)); if (!this.selectedChannel) { this.selectedChannel = channel; } this.channelAddedEmitter.fire(channel); return channel; } registerListeners(channel) { const { name } = channel; return new core_1.DisposableCollection(channel, channel.onVisibilityChange(({ isVisible, preserveFocus }) => { if (isVisible) { this.selectedChannel = channel; this.channelWasShownEmitter.fire({ name, preserveFocus }); } else { if (channel === this.selectedChannel) { this.selectedChannel = this.getVisibleChannels()[0]; } this.channelWasHiddenEmitter.fire({ name }); } }), channel.onDisposed(() => this.deleteChannel(name)), core_1.Disposable.create(() => { const resource = this.resources.get(name); if (resource) { resource.dispose(); this.resources.delete(name); } else { console.warn(`Could not dispose. No resource was for output channel: '${name}'.`); } }), core_1.Disposable.create(() => { const toDispose = this.channels.get(name); if (!toDispose) { console.warn(`Could not dispose. No channel exist with name: '${name}'.`); return; } this.channels.delete(name); toDispose.dispose(); this.channelDeletedEmitter.fire({ name }); if (this.selectedChannel && this.selectedChannel.name === name) { this.selectedChannel = this.getVisibleChannels()[0]; } })); } deleteChannel(name) { const toDispose = this.toDisposeOnChannelDeletion.get(name); if (toDispose) { toDispose.dispose(); } } getChannels() { return Array.from(this.channels.values()).sort(this.channelComparator); } getVisibleChannels() { return this.getChannels().filter(channel => channel.isVisible); } get channelComparator() { return (left, right) => { if (left.isVisible !== right.isVisible) { return left.isVisible ? -1 : 1; } return left.name.toLocaleLowerCase().localeCompare(right.name.toLocaleLowerCase()); }; } dispose() { this.toDispose.dispose(); } get selectedChannel() { return this._selectedChannel; } set selectedChannel(channel) { this._selectedChannel = channel; if (this._selectedChannel) { this.selectedChannelChangedEmitter.fire({ name: this._selectedChannel.name }); } else { this.selectedChannelChangedEmitter.fire(undefined); } } /** * Non-API: do not call directly. */ async resolve(uri) { if (!output_uri_1.OutputUri.is(uri)) { throw new Error(`Expected '${output_uri_1.OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`); } const resource = this.resources.get(output_uri_1.OutputUri.channelName(uri)); if (!resource) { throw new Error(`No output resource was registered with URI: ${uri.toString()}`); } return resource; } createResource({ uri, editorModelRef }) { return new output_resource_1.OutputResource(uri, editorModelRef); } createChannel(resource) { return new OutputChannel(resource, this.preferences); } }; exports.OutputChannelManager = OutputChannelManager; tslib_1.__decorate([ (0, inversify_1.inject)(monaco_text_model_service_1.MonacoTextModelService), tslib_1.__metadata("design:type", monaco_text_model_service_1.MonacoTextModelService) ], OutputChannelManager.prototype, "textModelService", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(output_preferences_1.OutputPreferences), tslib_1.__metadata("design:type", Object) ], OutputChannelManager.prototype, "preferences", void 0); exports.OutputChannelManager = OutputChannelManager = tslib_1.__decorate([ (0, inversify_1.injectable)() ], OutputChannelManager); var OutputChannelSeverity; (function (OutputChannelSeverity) { OutputChannelSeverity[OutputChannelSeverity["Error"] = 1] = "Error"; OutputChannelSeverity[OutputChannelSeverity["Warning"] = 2] = "Warning"; OutputChannelSeverity[OutputChannelSeverity["Info"] = 3] = "Info"; })(OutputChannelSeverity || (exports.OutputChannelSeverity = OutputChannelSeverity = {})); class OutputChannel { constructor(resource, preferences) { this.resource = resource; this.preferences = preferences; this.contentChangeEmitter = new core_1.Emitter(); this.visibilityChangeEmitter = new core_1.Emitter(); this.disposedEmitter = new core_1.Emitter(); this.textModifyQueue = new p_queue_1.default({ autoStart: true, concurrency: 1 }); this.toDispose = new core_1.DisposableCollection(core_1.Disposable.create(() => this.textModifyQueue.clear()), this.contentChangeEmitter, this.visibilityChangeEmitter, this.disposedEmitter); this.disposed = false; this.visible = true; this.decorationIds = new Set(); this.onVisibilityChange = this.visibilityChangeEmitter.event; this.onContentChange = this.contentChangeEmitter.event; this.onDisposed = this.disposedEmitter.event; this._maxLineNumber = this.preferences['output.maxChannelHistory']; this.toDispose.push(resource); this.toDispose.push(core_1.Disposable.create(() => this.decorationIds.clear())); this.toDispose.push(this.preferences.onPreferenceChanged(event => { if (event.preferenceName === 'output.maxChannelHistory') { const maxLineNumber = this.preferences['output.maxChannelHistory']; if (this.maxLineNumber !== maxLineNumber) { this.maxLineNumber = maxLineNumber; } } })); } get name() { return output_uri_1.OutputUri.channelName(this.uri); } get uri() { return this.resource.uri; } hide() { this.visible = false; this.visibilityChangeEmitter.fire({ isVisible: this.isVisible }); } /** * If `preserveFocus` is `true`, the channel will not take focus. It is `false` by default. * - Calling `show` without args or with `preserveFocus: false` will reveal **and** activate the `Output` widget. * - Calling `show` with `preserveFocus: true` will reveal the `Output` widget but **won't** activate it. */ show({ preserveFocus } = { preserveFocus: false }) { this.visible = true; this.visibilityChangeEmitter.fire({ isVisible: this.isVisible, preserveFocus }); } /** * Note: if `false` it does not meant it is disposed or not available, it is only hidden from the UI. */ get isVisible() { return this.visible; } clear() { this.textModifyQueue.add(async () => { const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel; textModel.deltaDecorations(Array.from(this.decorationIds), []); this.decorationIds.clear(); textModel.setValue(''); this.contentChangeEmitter.fire(); }); } dispose() { if (this.disposed) { return; } this.disposed = true; this.toDispose.dispose(); this.disposedEmitter.fire(); } append(content, severity = OutputChannelSeverity.Info) { this.textModifyQueue.add(() => this.doAppend({ content, severity })); } appendLine(content, severity = OutputChannelSeverity.Info) { this.textModifyQueue.add(() => this.doAppend({ content, severity, appendEol: true })); } async doAppend({ content, severity, appendEol }) { const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel; const lastLine = textModel.getLineCount(); const lastLineMaxColumn = textModel.getLineMaxColumn(lastLine); const position = new monaco.Position(lastLine, lastLineMaxColumn); const range = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column); const edits = [{ range, text: !!appendEol ? `${content}${textModel.getEOL()}` : content, forceMoveMarkers: true }]; // We do not use `pushEditOperations` as we do not need undo/redo support. VS Code uses `applyEdits` too. // https://github.com/microsoft/vscode/blob/dc348340fd1a6c583cb63a1e7e6b4fd657e01e01/src/vs/workbench/services/output/common/outputChannelModel.ts#L108-L115 textModel.applyEdits(edits); if (severity !== OutputChannelSeverity.Info) { const inlineClassName = severity === OutputChannelSeverity.Error ? 'theia-output-error' : 'theia-output-warning'; let endLineNumber = textModel.getLineCount(); // If last line is empty (the first non-whitespace is 0), apply decorator to previous line's last non-whitespace instead // Note: if the user appends `inlineWarning `, the new decorator's range includes the trailing whitespace. if (!textModel.getLineFirstNonWhitespaceColumn(endLineNumber)) { endLineNumber--; } const endColumn = textModel.getLineLastNonWhitespaceColumn(endLineNumber); const newDecorations = [{ range: new monaco.Range(range.startLineNumber, range.startColumn, endLineNumber, endColumn), options: { inlineClassName } }]; for (const decorationId of textModel.deltaDecorations([], newDecorations)) { this.decorationIds.add(decorationId); } } this.ensureMaxChannelHistory(textModel); this.contentChangeEmitter.fire(); } ensureMaxChannelHistory(textModel) { this.contentChangeEmitter.fire(); const linesToRemove = textModel.getLineCount() - this.maxLineNumber - 1; // -1 as the last line is usually empty -> `appendLine`. if (linesToRemove > 0) { const endColumn = textModel.getLineMaxColumn(linesToRemove); // `endLineNumber` is `linesToRemove` + 1 as monaco is one based. const range = new monaco.Range(1, 1, linesToRemove, endColumn + 1); // eslint-disable-next-line no-null/no-null const text = null; const decorationsToRemove = textModel.getLinesDecorations(range.startLineNumber, range.endLineNumber) .filter(({ id }) => this.decorationIds.has(id)).map(({ id }) => id); // Do we need to filter here? Who else can put decorations to the output model? if (decorationsToRemove.length) { for (const newId of textModel.deltaDecorations(decorationsToRemove, [])) { this.decorationIds.add(newId); } for (const toRemoveId of decorationsToRemove) { this.decorationIds.delete(toRemoveId); } } textModel.applyEdits([ { range: new monaco.Range(1, 1, linesToRemove + 1, textModel.getLineFirstNonWhitespaceColumn(linesToRemove + 1)), text, forceMoveMarkers: true } ]); } } get maxLineNumber() { return this._maxLineNumber; } set maxLineNumber(maxLineNumber) { this._maxLineNumber = maxLineNumber; this.append(''); // will trigger an `ensureMaxChannelHistory` call and will refresh the content. } } exports.OutputChannel = OutputChannel; //# sourceMappingURL=output-channel.js.map