deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
366
packages/output/src/browser/output-channel.ts
Normal file
366
packages/output/src/browser/output-channel.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Resource, ResourceResolver } from '@theia/core/lib/common/resource';
|
||||
import { Emitter, Event, Disposable, DisposableCollection } from '@theia/core';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { OutputUri } from '../common/output-uri';
|
||||
import { OutputResource } from '../browser/output-resource';
|
||||
import { OutputPreferences } from '../common/output-preferences';
|
||||
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
@injectable()
|
||||
export class OutputChannelManager implements Disposable, ResourceResolver {
|
||||
|
||||
@inject(MonacoTextModelService)
|
||||
protected readonly textModelService: MonacoTextModelService;
|
||||
|
||||
@inject(OutputPreferences)
|
||||
protected readonly preferences: OutputPreferences;
|
||||
|
||||
protected readonly channels = new Map<string, OutputChannel>();
|
||||
protected readonly resources = new Map<string, OutputResource>();
|
||||
protected _selectedChannel: OutputChannel | undefined;
|
||||
|
||||
protected readonly channelAddedEmitter = new Emitter<{ name: string }>();
|
||||
protected readonly channelDeletedEmitter = new Emitter<{ name: string }>();
|
||||
protected readonly channelWasShownEmitter = new Emitter<{ name: string, preserveFocus?: boolean }>();
|
||||
protected readonly channelWasHiddenEmitter = new Emitter<{ name: string }>();
|
||||
protected readonly selectedChannelChangedEmitter = new Emitter<{ name: string } | undefined>();
|
||||
|
||||
readonly onChannelAdded = this.channelAddedEmitter.event;
|
||||
readonly onChannelDeleted = this.channelDeletedEmitter.event;
|
||||
readonly onChannelWasShown = this.channelWasShownEmitter.event;
|
||||
readonly onChannelWasHidden = this.channelWasHiddenEmitter.event;
|
||||
readonly onSelectedChannelChanged = this.selectedChannelChangedEmitter.event;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
protected readonly toDisposeOnChannelDeletion = new Map<string, Disposable>();
|
||||
|
||||
getChannel(name: string): OutputChannel {
|
||||
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 = OutputUri.create(name);
|
||||
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
|
||||
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;
|
||||
}
|
||||
|
||||
protected registerListeners(channel: OutputChannel): Disposable {
|
||||
const { name } = channel;
|
||||
return new 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)),
|
||||
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}'.`);
|
||||
}
|
||||
}),
|
||||
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: string): void {
|
||||
const toDispose = this.toDisposeOnChannelDeletion.get(name);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
getChannels(): OutputChannel[] {
|
||||
return Array.from(this.channels.values()).sort(this.channelComparator);
|
||||
}
|
||||
|
||||
getVisibleChannels(): OutputChannel[] {
|
||||
return this.getChannels().filter(channel => channel.isVisible);
|
||||
}
|
||||
|
||||
protected get channelComparator(): (left: OutputChannel, right: OutputChannel) => number {
|
||||
return (left, right) => {
|
||||
if (left.isVisible !== right.isVisible) {
|
||||
return left.isVisible ? -1 : 1;
|
||||
}
|
||||
return left.name.toLocaleLowerCase().localeCompare(right.name.toLocaleLowerCase());
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get selectedChannel(): OutputChannel | undefined {
|
||||
return this._selectedChannel;
|
||||
}
|
||||
|
||||
set selectedChannel(channel: OutputChannel | undefined) {
|
||||
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: URI): Promise<Resource> {
|
||||
if (!OutputUri.is(uri)) {
|
||||
throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`);
|
||||
}
|
||||
const resource = this.resources.get(OutputUri.channelName(uri));
|
||||
if (!resource) {
|
||||
throw new Error(`No output resource was registered with URI: ${uri.toString()}`);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected createResource({ uri, editorModelRef }: { uri: URI, editorModelRef: Deferred<IReference<MonacoEditorModel>> }): OutputResource {
|
||||
return new OutputResource(uri, editorModelRef);
|
||||
}
|
||||
|
||||
protected createChannel(resource: OutputResource): OutputChannel {
|
||||
return new OutputChannel(resource, this.preferences);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export enum OutputChannelSeverity {
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Info = 3
|
||||
}
|
||||
|
||||
export class OutputChannel implements Disposable {
|
||||
|
||||
protected readonly contentChangeEmitter = new Emitter<void>();
|
||||
protected readonly visibilityChangeEmitter = new Emitter<{ isVisible: boolean, preserveFocus?: boolean }>();
|
||||
protected readonly disposedEmitter = new Emitter<void>();
|
||||
protected readonly textModifyQueue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.textModifyQueue.clear()),
|
||||
this.contentChangeEmitter,
|
||||
this.visibilityChangeEmitter,
|
||||
this.disposedEmitter
|
||||
);
|
||||
|
||||
protected disposed = false;
|
||||
protected visible = true;
|
||||
protected _maxLineNumber: number;
|
||||
protected decorationIds = new Set<string>();
|
||||
|
||||
readonly onVisibilityChange: Event<{ isVisible: boolean, preserveFocus?: boolean }> = this.visibilityChangeEmitter.event;
|
||||
readonly onContentChange: Event<void> = this.contentChangeEmitter.event;
|
||||
readonly onDisposed: Event<void> = this.disposedEmitter.event;
|
||||
|
||||
constructor(protected readonly resource: OutputResource, protected readonly preferences: OutputPreferences) {
|
||||
this._maxLineNumber = this.preferences['output.maxChannelHistory'];
|
||||
this.toDispose.push(resource);
|
||||
this.toDispose.push(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(): string {
|
||||
return OutputUri.channelName(this.uri);
|
||||
}
|
||||
|
||||
get uri(): URI {
|
||||
return this.resource.uri;
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
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: boolean } = { preserveFocus: false }): void {
|
||||
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(): boolean {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
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(): void {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
this.disposed = true;
|
||||
this.toDispose.dispose();
|
||||
this.disposedEmitter.fire();
|
||||
}
|
||||
|
||||
append(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
||||
this.textModifyQueue.add(() => this.doAppend({ content, severity }));
|
||||
}
|
||||
|
||||
appendLine(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
||||
this.textModifyQueue.add(() => this.doAppend({ content, severity, appendEol: true }));
|
||||
}
|
||||
|
||||
protected async doAppend({ content, severity, appendEol }: { content: string, severity: OutputChannelSeverity, appendEol?: boolean }): Promise<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
protected ensureMaxChannelHistory(textModel: monaco.editor.ITextModel): void {
|
||||
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
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected get maxLineNumber(): number {
|
||||
return this._maxLineNumber;
|
||||
}
|
||||
|
||||
protected set maxLineNumber(maxLineNumber: number) {
|
||||
this._maxLineNumber = maxLineNumber;
|
||||
this.append(''); // will trigger an `ensureMaxChannelHistory` call and will refresh the content.
|
||||
}
|
||||
|
||||
}
|
||||
100
packages/output/src/browser/output-commands.ts
Normal file
100
packages/output/src/browser/output-commands.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
import { Command, nls } from '@theia/core/lib/common';
|
||||
|
||||
export namespace OutputCommands {
|
||||
|
||||
const OUTPUT_CATEGORY = 'Output';
|
||||
const OUTPUT_CATEGORY_KEY = nls.getDefaultKey(OUTPUT_CATEGORY);
|
||||
|
||||
/* #region VS Code `OutputChannel` API */
|
||||
// Based on: https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/vscode.d.ts#L4692-L4745
|
||||
|
||||
export const APPEND: Command = {
|
||||
id: 'output:append'
|
||||
};
|
||||
|
||||
export const APPEND_LINE: Command = {
|
||||
id: 'output:appendLine'
|
||||
};
|
||||
|
||||
export const CLEAR: Command = {
|
||||
id: 'output:clear'
|
||||
};
|
||||
|
||||
export const SHOW: Command = {
|
||||
id: 'output:show'
|
||||
};
|
||||
|
||||
export const HIDE: Command = {
|
||||
id: 'output:hide'
|
||||
};
|
||||
|
||||
export const DISPOSE: Command = {
|
||||
id: 'output:dispose'
|
||||
};
|
||||
|
||||
/* #endregion VS Code `OutputChannel` API */
|
||||
|
||||
export const CLEAR__WIDGET = Command.toLocalizedCommand({
|
||||
id: 'output:widget:clear',
|
||||
category: OUTPUT_CATEGORY,
|
||||
iconClass: codicon('clear-all')
|
||||
}, '', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const LOCK__WIDGET = Command.toLocalizedCommand({
|
||||
id: 'output:widget:lock',
|
||||
category: OUTPUT_CATEGORY,
|
||||
iconClass: codicon('unlock')
|
||||
}, '', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const UNLOCK__WIDGET = Command.toLocalizedCommand({
|
||||
id: 'output:widget:unlock',
|
||||
category: OUTPUT_CATEGORY,
|
||||
iconClass: codicon('lock')
|
||||
}, '', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const CLEAR__QUICK_PICK = Command.toLocalizedCommand({
|
||||
id: 'output:pick-clear',
|
||||
label: 'Clear Output Channel...',
|
||||
category: OUTPUT_CATEGORY
|
||||
}, 'theia/output/clearOutputChannel', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const SHOW__QUICK_PICK = Command.toLocalizedCommand({
|
||||
id: 'output:pick-show',
|
||||
label: 'Show Output Channel...',
|
||||
category: OUTPUT_CATEGORY
|
||||
}, 'theia/output/showOutputChannel', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const HIDE__QUICK_PICK = Command.toLocalizedCommand({
|
||||
id: 'output:pick-hide',
|
||||
label: 'Hide Output Channel...',
|
||||
category: OUTPUT_CATEGORY
|
||||
}, 'theia/output/hideOutputChannel', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const DISPOSE__QUICK_PICK = Command.toLocalizedCommand({
|
||||
id: 'output:pick-dispose',
|
||||
label: 'Close Output Channel...',
|
||||
category: OUTPUT_CATEGORY
|
||||
}, 'theia/output/closeOutputChannel', OUTPUT_CATEGORY_KEY);
|
||||
|
||||
export const COPY_ALL: Command = {
|
||||
id: 'output:copy-all',
|
||||
};
|
||||
|
||||
}
|
||||
34
packages/output/src/browser/output-context-menu.ts
Normal file
34
packages/output/src/browser/output-context-menu.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core/lib/common';
|
||||
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';
|
||||
|
||||
export namespace OutputContextMenu {
|
||||
export const MENU_PATH: MenuPath = ['output_context_menu'];
|
||||
export const TEXT_EDIT_GROUP = [...MENU_PATH, '0_text_edit_group'];
|
||||
export const COMMAND_GROUP = [...MENU_PATH, '1_command_group'];
|
||||
export const WIDGET_GROUP = [...MENU_PATH, '2_widget_group'];
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class OutputContextMenuService extends MonacoContextMenuService {
|
||||
|
||||
protected override menuPath(): MenuPath {
|
||||
return OutputContextMenu.MENU_PATH;
|
||||
}
|
||||
|
||||
}
|
||||
274
packages/output/src/browser/output-contribution.ts
Normal file
274
packages/output/src/browser/output-contribution.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Widget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { CommonCommands, quickCommand, OpenHandler, open, OpenerOptions, OpenerService, QuickPickItem, QuickPickValue } from '@theia/core/lib/browser';
|
||||
import { CommandRegistry, MenuModelRegistry, CommandService } from '@theia/core/lib/common';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { OutputWidget } from './output-widget';
|
||||
import { OutputContextMenu } from './output-context-menu';
|
||||
import { OutputUri } from '../common/output-uri';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { OutputChannelManager, OutputChannel } from './output-channel';
|
||||
import { OutputCommands } from './output-commands';
|
||||
import { QuickPickSeparator, QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class OutputContribution extends AbstractViewContribution<OutputWidget> implements OpenHandler {
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(QuickPickService)
|
||||
protected readonly quickPickService: QuickPickService;
|
||||
|
||||
readonly id: string = `${OutputWidget.ID}-opener`;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: OutputWidget.ID,
|
||||
widgetName: OutputWidget.LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'bottom'
|
||||
},
|
||||
toggleCommandId: 'output:toggle',
|
||||
toggleKeybinding: 'CtrlCmd+Shift+U'
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.outputChannelManager.onChannelWasShown(({ name, preserveFocus }) =>
|
||||
open(this.openerService, OutputUri.create(name), { activate: !preserveFocus, reveal: true }));
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
super.registerCommands(registry);
|
||||
registry.registerCommand(OutputCommands.CLEAR__WIDGET, {
|
||||
isEnabled: arg => {
|
||||
if (arg instanceof Widget) {
|
||||
return arg instanceof OutputWidget;
|
||||
}
|
||||
return this.shell.currentWidget instanceof OutputWidget;
|
||||
},
|
||||
isVisible: arg => {
|
||||
if (arg instanceof Widget) {
|
||||
return arg instanceof OutputWidget;
|
||||
}
|
||||
return this.shell.currentWidget instanceof OutputWidget;
|
||||
},
|
||||
execute: () => {
|
||||
this.widget.then(widget => {
|
||||
this.withWidget(widget, output => {
|
||||
output.clear();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.LOCK__WIDGET, {
|
||||
isEnabled: widget => this.withWidget(widget, output => !output.isLocked),
|
||||
isVisible: widget => this.withWidget(widget, output => !output.isLocked),
|
||||
execute: widget => this.withWidget(widget, output => {
|
||||
output.lock();
|
||||
return true;
|
||||
})
|
||||
});
|
||||
registry.registerCommand(OutputCommands.UNLOCK__WIDGET, {
|
||||
isEnabled: widget => this.withWidget(widget, output => output.isLocked),
|
||||
isVisible: widget => this.withWidget(widget, output => output.isLocked),
|
||||
execute: widget => this.withWidget(widget, output => {
|
||||
output.unlock();
|
||||
return true;
|
||||
})
|
||||
});
|
||||
registry.registerCommand(OutputCommands.COPY_ALL, {
|
||||
execute: () => {
|
||||
const textToCopy = this.tryGetWidget()?.getText();
|
||||
if (textToCopy) {
|
||||
this.clipboardService.writeText(textToCopy);
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.APPEND, {
|
||||
execute: ({ name, text }: { name: string, text: string }) => {
|
||||
if (name && text) {
|
||||
this.outputChannelManager.getChannel(name).append(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.APPEND_LINE, {
|
||||
execute: ({ name, text }: { name: string, text: string }) => {
|
||||
if (name && text) {
|
||||
this.outputChannelManager.getChannel(name).appendLine(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.CLEAR, {
|
||||
execute: ({ name }: { name: string }) => {
|
||||
if (name) {
|
||||
this.outputChannelManager.getChannel(name).clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.DISPOSE, {
|
||||
execute: ({ name }: { name: string }) => {
|
||||
if (name) {
|
||||
this.outputChannelManager.deleteChannel(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.SHOW, {
|
||||
execute: ({ name, options }: { name: string, options?: { preserveFocus?: boolean } }) => {
|
||||
if (name) {
|
||||
const preserveFocus = options && options.preserveFocus || false;
|
||||
this.outputChannelManager.getChannel(name).show({ preserveFocus });
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(OutputCommands.HIDE, {
|
||||
execute: ({ name }: { name: string }) => {
|
||||
if (name) {
|
||||
this.outputChannelManager.getChannel(name).hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(OutputCommands.CLEAR__QUICK_PICK, {
|
||||
execute: async () => {
|
||||
const channel = await this.pick({
|
||||
placeholder: OutputCommands.CLEAR__QUICK_PICK.label!,
|
||||
channels: this.outputChannelManager.getChannels().slice()
|
||||
});
|
||||
if (channel) {
|
||||
channel.clear();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.outputChannelManager.getChannels().length,
|
||||
isVisible: () => !!this.outputChannelManager.getChannels().length
|
||||
});
|
||||
registry.registerCommand(OutputCommands.SHOW__QUICK_PICK, {
|
||||
execute: async () => {
|
||||
const channel = await this.pick({
|
||||
placeholder: OutputCommands.SHOW__QUICK_PICK.label!,
|
||||
channels: this.outputChannelManager.getChannels().slice()
|
||||
});
|
||||
if (channel) {
|
||||
const { name } = channel;
|
||||
registry.executeCommand(OutputCommands.SHOW.id, { name, options: { preserveFocus: false } });
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.outputChannelManager.getChannels().length,
|
||||
isVisible: () => !!this.outputChannelManager.getChannels().length
|
||||
});
|
||||
registry.registerCommand(OutputCommands.HIDE__QUICK_PICK, {
|
||||
execute: async () => {
|
||||
const channel = await this.pick({
|
||||
placeholder: OutputCommands.HIDE__QUICK_PICK.label!,
|
||||
channels: this.outputChannelManager.getVisibleChannels().slice()
|
||||
});
|
||||
if (channel) {
|
||||
const { name } = channel;
|
||||
registry.executeCommand(OutputCommands.HIDE.id, { name });
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.outputChannelManager.getVisibleChannels().length,
|
||||
isVisible: () => !!this.outputChannelManager.getVisibleChannels().length
|
||||
});
|
||||
registry.registerCommand(OutputCommands.DISPOSE__QUICK_PICK, {
|
||||
execute: async () => {
|
||||
const channel = await this.pick({
|
||||
placeholder: OutputCommands.DISPOSE__QUICK_PICK.label!,
|
||||
channels: this.outputChannelManager.getChannels().slice()
|
||||
});
|
||||
if (channel) {
|
||||
const { name } = channel;
|
||||
registry.executeCommand(OutputCommands.DISPOSE.id, { name });
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.outputChannelManager.getChannels().length,
|
||||
isVisible: () => !!this.outputChannelManager.getChannels().length
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
super.registerMenus(registry);
|
||||
registry.registerMenuAction(OutputContextMenu.TEXT_EDIT_GROUP, {
|
||||
commandId: CommonCommands.COPY.id
|
||||
});
|
||||
registry.registerMenuAction(OutputContextMenu.TEXT_EDIT_GROUP, {
|
||||
commandId: OutputCommands.COPY_ALL.id,
|
||||
label: nls.localizeByDefault('Copy All')
|
||||
});
|
||||
registry.registerMenuAction(OutputContextMenu.COMMAND_GROUP, {
|
||||
commandId: quickCommand.id,
|
||||
label: nls.localizeByDefault('Command Palette...')
|
||||
});
|
||||
registry.registerMenuAction(OutputContextMenu.WIDGET_GROUP, {
|
||||
commandId: OutputCommands.CLEAR__WIDGET.id,
|
||||
label: nls.localizeByDefault('Clear Output')
|
||||
});
|
||||
}
|
||||
|
||||
canHandle(uri: URI): MaybePromise<number> {
|
||||
return OutputUri.is(uri) ? 200 : 0;
|
||||
}
|
||||
|
||||
async open(uri: URI, options?: OpenerOptions): Promise<OutputWidget> {
|
||||
if (!OutputUri.is(uri)) {
|
||||
throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`);
|
||||
}
|
||||
const widget = await this.openView(options);
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected withWidget(
|
||||
widget: Widget | undefined = this.tryGetWidget(),
|
||||
predicate: (output: OutputWidget) => boolean = () => true
|
||||
): boolean | false {
|
||||
return widget instanceof OutputWidget ? predicate(widget) : false;
|
||||
}
|
||||
|
||||
protected async pick({ channels, placeholder }: { channels: OutputChannel[], placeholder: string }): Promise<OutputChannel | undefined> {
|
||||
const items: Array<QuickPickValue<OutputChannel> | QuickPickItem | QuickPickSeparator> = [];
|
||||
const outputChannels = nls.localize('theia/output/outputChannels', 'Output Channels');
|
||||
const hiddenChannels = nls.localize('theia/output/hiddenChannels', 'Hidden Channels');
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const channel = channels[i];
|
||||
if (i === 0) {
|
||||
items.push({ label: channel.isVisible ? outputChannels : hiddenChannels, type: 'separator' });
|
||||
} else if (!channel.isVisible && channels[i - 1].isVisible) {
|
||||
items.push({ label: hiddenChannels, type: 'separator' });
|
||||
}
|
||||
items.push({ label: channel.name, value: channel });
|
||||
}
|
||||
const selectedItem = await this.quickPickService.show(items, { placeholder });
|
||||
return selectedItem && ('value' in selectedItem) ? selectedItem.value : undefined;
|
||||
}
|
||||
}
|
||||
68
packages/output/src/browser/output-editor-factory.ts
Normal file
68
packages/output/src/browser/output-editor-factory.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoEditorFactory } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';
|
||||
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { OutputUri } from '../common/output-uri';
|
||||
import { OutputContextMenuService } from './output-context-menu';
|
||||
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
|
||||
|
||||
@injectable()
|
||||
export class OutputEditorFactory implements MonacoEditorFactory {
|
||||
|
||||
@inject(MonacoEditorServices)
|
||||
protected readonly services: MonacoEditorServices;
|
||||
|
||||
@inject(OutputContextMenuService)
|
||||
protected readonly contextMenuService: MonacoContextMenuService;
|
||||
|
||||
readonly scheme: string = OutputUri.SCHEME;
|
||||
|
||||
create(model: MonacoEditorModel, defaultsOptions: MonacoEditor.IOptions): Promise<MonacoEditor> {
|
||||
const uri = new URI(model.uri);
|
||||
const options = this.createOptions(model, defaultsOptions);
|
||||
const overrides = this.createOverrides(model);
|
||||
return MonacoEditor.create(uri, model, document.createElement('div'), this.services, options, overrides);
|
||||
}
|
||||
|
||||
protected createOptions(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions): MonacoEditor.IOptions {
|
||||
return {
|
||||
...defaultOptions,
|
||||
overviewRulerLanes: 3,
|
||||
lineNumbersMinChars: 3,
|
||||
fixedOverflowWidgets: true,
|
||||
wordWrap: 'off',
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
lineDecorationsWidth: 20,
|
||||
rulers: [],
|
||||
folding: false,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
renderLineHighlight: 'none',
|
||||
minimap: { enabled: false },
|
||||
matchBrackets: 'never'
|
||||
};
|
||||
}
|
||||
|
||||
protected *createOverrides(model: MonacoEditorModel): EditorServiceOverrides {
|
||||
yield [IContextMenuService, this.contextMenuService];
|
||||
}
|
||||
}
|
||||
54
packages/output/src/browser/output-editor-model-factory.ts
Normal file
54
packages/output/src/browser/output-editor-model-factory.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Resource } from '@theia/core/lib/common/resource';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { OutputUri } from '../common/output-uri';
|
||||
import { MonacoEditorModelFactory } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||
|
||||
@injectable()
|
||||
export class OutputEditorModelFactory implements MonacoEditorModelFactory {
|
||||
|
||||
@inject(MonacoToProtocolConverter)
|
||||
protected readonly m2p: MonacoToProtocolConverter;
|
||||
|
||||
@inject(ProtocolToMonacoConverter)
|
||||
protected readonly p2m: ProtocolToMonacoConverter;
|
||||
|
||||
readonly scheme: string = OutputUri.SCHEME;
|
||||
|
||||
createModel(
|
||||
resource: Resource
|
||||
): MonacoEditorModel {
|
||||
return new OutputEditorModel(resource, this.m2p, this.p2m);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class OutputEditorModel extends MonacoEditorModel {
|
||||
|
||||
override get readOnly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override setDirty(dirty: boolean): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
}
|
||||
53
packages/output/src/browser/output-frontend-module.ts
Normal file
53
packages/output/src/browser/output-frontend-module.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { OutputWidget } from './output-widget';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { ResourceResolver } from '@theia/core/lib/common';
|
||||
import { WidgetFactory, bindViewContribution, OpenHandler } from '@theia/core/lib/browser';
|
||||
import { OutputChannelManager } from './output-channel';
|
||||
import { bindOutputPreferences } from '../common/output-preferences';
|
||||
import { OutputToolbarContribution } from './output-toolbar-contribution';
|
||||
import { OutputContribution } from './output-contribution';
|
||||
import { MonacoEditorFactory } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { OutputContextMenuService } from './output-context-menu';
|
||||
import { OutputEditorFactory } from './output-editor-factory';
|
||||
import { MonacoEditorModelFactory } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { OutputEditorModelFactory } from './output-editor-model-factory';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||
bind(ResourceResolver).toService(OutputChannelManager);
|
||||
bind(OutputEditorFactory).toSelf().inSingletonScope();
|
||||
bind(MonacoEditorFactory).toService(OutputEditorFactory);
|
||||
bind(OutputEditorModelFactory).toSelf().inSingletonScope();
|
||||
bind(MonacoEditorModelFactory).toService(OutputEditorModelFactory);
|
||||
bind(OutputContextMenuService).toSelf().inSingletonScope();
|
||||
|
||||
bindOutputPreferences(bind);
|
||||
|
||||
bind(OutputWidget).toSelf();
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: OutputWidget.ID,
|
||||
createWidget: () => context.container.get<OutputWidget>(OutputWidget)
|
||||
}));
|
||||
bindViewContribution(bind, OutputContribution);
|
||||
bind(OpenHandler).to(OutputContribution).inSingletonScope();
|
||||
|
||||
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
||||
bind(TabBarToolbarContribution).toService(OutputToolbarContribution);
|
||||
});
|
||||
65
packages/output/src/browser/output-resource.ts
Normal file
65
packages/output/src/browser/output-resource.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Event, Resource, ResourceReadOptions, DisposableCollection, Emitter } from '@theia/core/lib/common';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
export class OutputResource implements Resource {
|
||||
|
||||
protected _textModel: monaco.editor.ITextModel | undefined;
|
||||
protected onDidChangeContentsEmitter = new Emitter<void>();
|
||||
protected toDispose = new DisposableCollection(
|
||||
this.onDidChangeContentsEmitter
|
||||
);
|
||||
|
||||
constructor(readonly uri: URI, readonly editorModelRef: Deferred<IReference<MonacoEditorModel>>) {
|
||||
this.editorModelRef.promise.then(modelRef => {
|
||||
if (this.toDispose.disposed) {
|
||||
modelRef.dispose();
|
||||
return;
|
||||
}
|
||||
const textModel = modelRef.object.textEditorModel;
|
||||
this._textModel = textModel;
|
||||
this.toDispose.push(modelRef);
|
||||
this.toDispose.push(this._textModel!.onDidChangeContent(() => this.onDidChangeContentsEmitter.fire()));
|
||||
});
|
||||
}
|
||||
|
||||
get textModel(): monaco.editor.ITextModel | undefined {
|
||||
return this._textModel;
|
||||
}
|
||||
|
||||
get onDidChangeContents(): Event<void> {
|
||||
return this.onDidChangeContentsEmitter.event;
|
||||
}
|
||||
|
||||
async readContents(options?: ResourceReadOptions): Promise<string> {
|
||||
if (this._textModel) {
|
||||
const modelRef = await this.editorModelRef.promise;
|
||||
return modelRef.object.textEditorModel.getValue();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
116
packages/output/src/browser/output-toolbar-contribution.tsx
Normal file
116
packages/output/src/browser/output-toolbar-contribution.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Arm 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
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
||||
import { OutputWidget } from './output-widget';
|
||||
import { OutputCommands } from './output-commands';
|
||||
import { OutputContribution } from './output-contribution';
|
||||
import { OutputChannelManager } from './output-channel';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class OutputToolbarContribution implements TabBarToolbarContribution {
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(OutputContribution)
|
||||
protected readonly outputContribution: OutputContribution;
|
||||
|
||||
protected readonly onOutputWidgetStateChangedEmitter = new Emitter<void>();
|
||||
protected readonly onOutputWidgetStateChanged = this.onOutputWidgetStateChangedEmitter.event;
|
||||
|
||||
protected readonly onChannelsChangedEmitter = new Emitter<void>();
|
||||
protected readonly onChannelsChanged = this.onChannelsChangedEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.outputContribution.widget.then(widget => {
|
||||
widget.onStateChanged(() => this.onOutputWidgetStateChangedEmitter.fire());
|
||||
});
|
||||
const fireChannelsChanged = () => this.onChannelsChangedEmitter.fire();
|
||||
this.outputChannelManager.onSelectedChannelChanged(fireChannelsChanged);
|
||||
this.outputChannelManager.onChannelAdded(fireChannelsChanged);
|
||||
this.outputChannelManager.onChannelDeleted(fireChannelsChanged);
|
||||
this.outputChannelManager.onChannelWasShown(fireChannelsChanged);
|
||||
this.outputChannelManager.onChannelWasHidden(fireChannelsChanged);
|
||||
}
|
||||
|
||||
registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): void {
|
||||
toolbarRegistry.registerItem({
|
||||
id: 'channels',
|
||||
render: () => this.renderChannelSelector(),
|
||||
isVisible: widget => widget instanceof OutputWidget,
|
||||
onDidChange: this.onChannelsChanged
|
||||
});
|
||||
toolbarRegistry.registerItem({
|
||||
id: OutputCommands.CLEAR__WIDGET.id,
|
||||
command: OutputCommands.CLEAR__WIDGET.id,
|
||||
tooltip: nls.localizeByDefault('Clear Output'),
|
||||
priority: 1,
|
||||
});
|
||||
toolbarRegistry.registerItem({
|
||||
id: OutputCommands.LOCK__WIDGET.id,
|
||||
command: OutputCommands.LOCK__WIDGET.id,
|
||||
tooltip: nls.localizeByDefault('Turn Auto Scrolling Off'),
|
||||
onDidChange: this.onOutputWidgetStateChanged,
|
||||
priority: 2
|
||||
});
|
||||
toolbarRegistry.registerItem({
|
||||
id: OutputCommands.UNLOCK__WIDGET.id,
|
||||
command: OutputCommands.UNLOCK__WIDGET.id,
|
||||
tooltip: nls.localizeByDefault('Turn Auto Scrolling On'),
|
||||
onDidChange: this.onOutputWidgetStateChanged,
|
||||
priority: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly NONE = '<no channels>';
|
||||
protected readonly OUTPUT_CHANNEL_LIST_ID = 'outputChannelList';
|
||||
|
||||
protected renderChannelSelector(): React.ReactNode {
|
||||
const channelOptionElements: SelectOption[] = [];
|
||||
this.outputChannelManager.getVisibleChannels().forEach((channel, i) => {
|
||||
channelOptionElements.push({
|
||||
value: channel.name
|
||||
});
|
||||
});
|
||||
if (channelOptionElements.length === 0) {
|
||||
channelOptionElements.push({
|
||||
value: this.NONE
|
||||
});
|
||||
}
|
||||
return <div id={this.OUTPUT_CHANNEL_LIST_ID} key={this.OUTPUT_CHANNEL_LIST_ID}>
|
||||
<SelectComponent
|
||||
key={this.outputChannelManager.selectedChannel?.name}
|
||||
options={channelOptionElements}
|
||||
defaultValue={this.outputChannelManager.selectedChannel?.name}
|
||||
onChange={option => this.changeChannel(option)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected changeChannel = (option: SelectOption) => {
|
||||
const channelName = option.value;
|
||||
if (channelName !== this.NONE && channelName) {
|
||||
this.outputChannelManager.getChannel(channelName).show();
|
||||
}
|
||||
};
|
||||
}
|
||||
343
packages/output/src/browser/output-widget.ts
Normal file
343
packages/output/src/browser/output-widget.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import '../../src/browser/style/output.css';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { SelectionService } from '@theia/core/lib/common/selection-service';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Message, BaseWidget, DockPanel, Widget, MessageLoop, StatefulWidget, codicon, StorageService } from '@theia/core/lib/browser';
|
||||
import { OutputUri } from '../common/output-uri';
|
||||
import { OutputChannelManager, OutputChannel } from './output-channel';
|
||||
import { Emitter, Event, deepClone } from '@theia/core';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
@injectable()
|
||||
export class OutputWidget extends BaseWidget implements StatefulWidget {
|
||||
|
||||
static readonly ID = 'outputView';
|
||||
static readonly LABEL = nls.localizeByDefault('Output');
|
||||
static readonly SELECTED_CHANNEL_STORAGE_KEY = 'output-widget-selected-channel';
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
|
||||
@inject(MonacoEditorProvider)
|
||||
protected readonly editorProvider: MonacoEditorProvider;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(StorageService)
|
||||
protected readonly storageService: StorageService;
|
||||
|
||||
protected _state: OutputWidget.State = { locked: false };
|
||||
protected readonly editorContainer: DockPanel;
|
||||
protected readonly toDisposeOnSelectedChannelChanged = new DisposableCollection();
|
||||
protected readonly onStateChangedEmitter = new Emitter<OutputWidget.State>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = OutputWidget.ID;
|
||||
this.title.label = OutputWidget.LABEL;
|
||||
this.title.caption = OutputWidget.LABEL;
|
||||
this.title.iconClass = codicon('output');
|
||||
this.title.closable = true;
|
||||
this.addClass('theia-output');
|
||||
this.node.tabIndex = 0;
|
||||
this.editorContainer = new NoopDragOverDockPanel({ spacing: 0, mode: 'single-document' });
|
||||
this.editorContainer.addClass('editor-container');
|
||||
this.editorContainer.node.tabIndex = -1;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.outputChannelManager.onChannelAdded(({ name }) => {
|
||||
this.tryRestorePendingChannel(name);
|
||||
this.refreshEditorWidget();
|
||||
}),
|
||||
this.outputChannelManager.onChannelDeleted(() => this.refreshEditorWidget()),
|
||||
this.outputChannelManager.onChannelWasHidden(() => this.refreshEditorWidget()),
|
||||
this.outputChannelManager.onChannelWasShown(({ preserveFocus }) => {
|
||||
// User explicitly showed a channel, clear any pending restoration
|
||||
// so we don't override their choice when the pending channel is registered later
|
||||
this.clearPendingChannelRestore();
|
||||
this.refreshEditorWidget({ preserveFocus: !!preserveFocus });
|
||||
}),
|
||||
this.outputChannelManager.onSelectedChannelChanged(() => this.refreshEditorWidget()),
|
||||
this.toDisposeOnSelectedChannelChanged,
|
||||
this.onStateChangedEmitter,
|
||||
this.onStateChanged(() => this.update())
|
||||
]);
|
||||
this.restoreSelectedChannelFromStorage();
|
||||
this.refreshEditorWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the selected channel from storage (used when widget is reopened).
|
||||
* State restoration has higher priority, so this only applies if state restoration hasn't already
|
||||
* set a selectedChannelName or pendingSelectedChannelName.
|
||||
*/
|
||||
protected async restoreSelectedChannelFromStorage(): Promise<void> {
|
||||
const storedChannelName = await this.storageService.getData<string>(OutputWidget.SELECTED_CHANNEL_STORAGE_KEY);
|
||||
// Only apply storage restoration if state restoration hasn't provided a channel
|
||||
if (storedChannelName && !this._state.selectedChannelName && !this._state.pendingSelectedChannelName) {
|
||||
const channel = this.outputChannelManager.getVisibleChannels().find(ch => ch.name === storedChannelName);
|
||||
if (channel) {
|
||||
this.outputChannelManager.selectedChannel = channel;
|
||||
this.refreshEditorWidget();
|
||||
} else {
|
||||
// Channel not yet available, store as pending
|
||||
this._state = { ...this._state, pendingSelectedChannelName: storedChannelName };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
// Save the selected channel to storage before disposing
|
||||
const channelName = this.selectedChannel?.name;
|
||||
if (channelName) {
|
||||
this.storageService.setData(OutputWidget.SELECTED_CHANNEL_STORAGE_KEY, channelName);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to restore the pending channel if it matches the newly added channel.
|
||||
*/
|
||||
protected tryRestorePendingChannel(addedChannelName: string): void {
|
||||
const pendingName = this._state.pendingSelectedChannelName;
|
||||
if (pendingName && pendingName === addedChannelName) {
|
||||
const channel = this.outputChannelManager.getVisibleChannels().find(ch => ch.name === pendingName);
|
||||
if (channel) {
|
||||
this.outputChannelManager.selectedChannel = channel;
|
||||
this.clearPendingChannelRestore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any pending channel restoration.
|
||||
* Called when the user explicitly selects a channel, so we don't override their choice.
|
||||
*/
|
||||
protected clearPendingChannelRestore(): void {
|
||||
if (this._state.pendingSelectedChannelName) {
|
||||
this._state = { ...this._state, pendingSelectedChannelName: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
storeState(): object {
|
||||
const { locked, selectedChannelName } = this.state;
|
||||
const result: OutputWidget.State = { locked };
|
||||
// Store the selected channel name, preferring the actual current selection
|
||||
// over any pending restoration that hasn't completed yet
|
||||
if (this.selectedChannel) {
|
||||
result.selectedChannelName = this.selectedChannel.name;
|
||||
} else if (selectedChannelName) {
|
||||
result.selectedChannelName = selectedChannelName;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
restoreState(oldState: object & Partial<OutputWidget.State>): void {
|
||||
const copy = deepClone(this.state);
|
||||
if (oldState.locked) {
|
||||
copy.locked = oldState.locked;
|
||||
}
|
||||
if (oldState.selectedChannelName) {
|
||||
copy.selectedChannelName = oldState.selectedChannelName;
|
||||
// Try to restore the selected channel in the manager if it exists
|
||||
const channels = this.outputChannelManager.getVisibleChannels();
|
||||
const channel = channels.find(ch => ch.name === oldState.selectedChannelName);
|
||||
if (channel) {
|
||||
this.outputChannelManager.selectedChannel = channel;
|
||||
} else {
|
||||
// Channel not yet available (e.g., registered by an extension that loads later).
|
||||
// Store as pending and wait for it to be added.
|
||||
copy.pendingSelectedChannelName = oldState.selectedChannelName;
|
||||
}
|
||||
}
|
||||
this.state = copy;
|
||||
}
|
||||
|
||||
protected get state(): OutputWidget.State {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
protected set state(state: OutputWidget.State) {
|
||||
this._state = state;
|
||||
this.onStateChangedEmitter.fire(this._state);
|
||||
}
|
||||
|
||||
protected async refreshEditorWidget({ preserveFocus }: { preserveFocus: boolean } = { preserveFocus: false }): Promise<void> {
|
||||
const { selectedChannel } = this;
|
||||
const editorWidget = this.editorWidget;
|
||||
if (selectedChannel && editorWidget) {
|
||||
// If the input is the current one, do nothing.
|
||||
const model = (editorWidget.editor as MonacoEditor).getControl().getModel();
|
||||
if (model && model.uri.toString() === selectedChannel.uri.toString()) {
|
||||
if (!preserveFocus) {
|
||||
this.activate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.toDisposeOnSelectedChannelChanged.dispose();
|
||||
if (selectedChannel) {
|
||||
const widget = await this.createEditorWidget();
|
||||
if (widget) {
|
||||
this.editorContainer.addWidget(widget);
|
||||
this.toDisposeOnSelectedChannelChanged.pushAll([
|
||||
Disposable.create(() => widget.close()),
|
||||
selectedChannel.onContentChange(() => this.revealLastLine())
|
||||
]);
|
||||
if (!preserveFocus) {
|
||||
this.activate();
|
||||
}
|
||||
this.revealLastLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
Widget.attach(this.editorContainer, this.node);
|
||||
this.toDisposeOnDetach.push(Disposable.create(() => Widget.detach(this.editorContainer)));
|
||||
}
|
||||
|
||||
protected override onActivateRequest(message: Message): void {
|
||||
super.onActivateRequest(message);
|
||||
if (this.editor) {
|
||||
this.editor.focus();
|
||||
} else {
|
||||
this.node.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected override onResize(message: Widget.ResizeMessage): void {
|
||||
super.onResize(message);
|
||||
MessageLoop.sendMessage(this.editorContainer, Widget.ResizeMessage.UnknownSize);
|
||||
for (const widget of this.editorContainer.widgets()) {
|
||||
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
|
||||
}
|
||||
}
|
||||
|
||||
protected override onAfterShow(msg: Message): void {
|
||||
super.onAfterShow(msg);
|
||||
this.onResize(Widget.ResizeMessage.UnknownSize); // Triggers an editor widget resize. (#8361)
|
||||
}
|
||||
|
||||
get onStateChanged(): Event<OutputWidget.State> {
|
||||
return this.onStateChangedEmitter.event;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (this.selectedChannel) {
|
||||
this.selectedChannel.clear();
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(): void {
|
||||
const editor = this.editor;
|
||||
if (editor) {
|
||||
const model = editor.getControl().getModel();
|
||||
if (model) {
|
||||
const endLine = model.getLineCount();
|
||||
const endCharacter = model.getLineMaxColumn(endLine);
|
||||
editor.getControl().setSelection(new monaco.Range(1, 1, endLine, endCharacter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock(): void {
|
||||
this.state = { ...deepClone(this.state), locked: true };
|
||||
}
|
||||
|
||||
unlock(): void {
|
||||
this.state = { ...deepClone(this.state), locked: false };
|
||||
}
|
||||
|
||||
get isLocked(): boolean {
|
||||
return !!this.state.locked;
|
||||
}
|
||||
|
||||
protected revealLastLine(): void {
|
||||
if (this.isLocked) {
|
||||
return;
|
||||
}
|
||||
const editor = this.editor;
|
||||
if (editor) {
|
||||
const model = editor.getControl().getModel();
|
||||
if (model) {
|
||||
const lineNumber = model.getLineCount();
|
||||
const column = model.getLineMaxColumn(lineNumber);
|
||||
editor.getControl().revealPosition({ lineNumber, column }, monaco.editor.ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get selectedChannel(): OutputChannel | undefined {
|
||||
return this.outputChannelManager.selectedChannel;
|
||||
}
|
||||
|
||||
private async createEditorWidget(): Promise<EditorWidget | undefined> {
|
||||
if (!this.selectedChannel) {
|
||||
return undefined;
|
||||
}
|
||||
const { name } = this.selectedChannel;
|
||||
const editor = await this.editorProvider.get(OutputUri.create(name));
|
||||
return new EditorWidget(editor, this.selectionService);
|
||||
}
|
||||
|
||||
private get editorWidget(): EditorWidget | undefined {
|
||||
for (const widget of this.editorContainer.children()) {
|
||||
if (widget instanceof EditorWidget) {
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private get editor(): MonacoEditor | undefined {
|
||||
return MonacoEditor.get(this.editorWidget);
|
||||
}
|
||||
|
||||
getText(): string | undefined {
|
||||
return this.editor?.getControl().getModel()?.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace OutputWidget {
|
||||
export interface State {
|
||||
locked?: boolean;
|
||||
selectedChannelName?: string;
|
||||
/** Channel name waiting to be restored when it becomes available */
|
||||
pendingSelectedChannelName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized `DockPanel` that does not allow dropping widgets into it.
|
||||
*/
|
||||
class NoopDragOverDockPanel extends DockPanel { }
|
||||
NoopDragOverDockPanel.prototype['_evtDragOver'] = () => { };
|
||||
NoopDragOverDockPanel.prototype['_evtDrop'] = () => { };
|
||||
NoopDragOverDockPanel.prototype['_evtDragLeave'] = () => { };
|
||||
32
packages/output/src/browser/style/output.css
Normal file
32
packages/output/src/browser/style/output.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/********************************************************************************
|
||||
* 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
|
||||
********************************************************************************/
|
||||
|
||||
.theia-output .editor-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theia-output .theia-output-error {
|
||||
color: var(--theia-errorForeground);
|
||||
}
|
||||
|
||||
.theia-output .theia-output-warning {
|
||||
color: var(--theia-editorWarning-foreground);
|
||||
}
|
||||
|
||||
#outputChannelList .theia-select-component {
|
||||
width: 170px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
57
packages/output/src/common/output-preferences.ts
Normal file
57
packages/output/src/common/output-preferences.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
PreferenceContribution,
|
||||
PreferenceProxy,
|
||||
PreferenceSchema,
|
||||
PreferenceService,
|
||||
createPreferenceProxy
|
||||
} from '@theia/core/lib/common/preferences';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export const OutputConfigSchema: PreferenceSchema = {
|
||||
'properties': {
|
||||
'output.maxChannelHistory': {
|
||||
'type': 'number',
|
||||
'description': nls.localize('theia/output/maxChannelHistory', 'The maximum number of entries in an output channel.'),
|
||||
'default': 1000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface OutputConfiguration {
|
||||
'output.maxChannelHistory': number
|
||||
}
|
||||
|
||||
export const OutputPreferenceContribution = Symbol('OutputPreferenceContribution');
|
||||
export const OutputPreferences = Symbol('OutputPreferences');
|
||||
export type OutputPreferences = PreferenceProxy<OutputConfiguration>;
|
||||
|
||||
export function createOutputPreferences(preferences: PreferenceService, schema: PreferenceSchema = OutputConfigSchema): OutputPreferences {
|
||||
return createPreferenceProxy(preferences, schema);
|
||||
}
|
||||
|
||||
export function bindOutputPreferences(bind: interfaces.Bind): void {
|
||||
bind(OutputPreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
const contribution = ctx.container.get<PreferenceContribution>(OutputPreferenceContribution);
|
||||
return createOutputPreferences(preferences, contribution.schema);
|
||||
}).inSingletonScope();
|
||||
bind(OutputPreferenceContribution).toConstantValue({ schema: OutputConfigSchema });
|
||||
bind(PreferenceContribution).toService(OutputPreferenceContribution);
|
||||
}
|
||||
53
packages/output/src/common/output-uri.spec.ts
Normal file
53
packages/output/src/common/output-uri.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { OutputUri } from './output-uri';
|
||||
import { fail } from 'assert';
|
||||
|
||||
describe('output-uri', () => {
|
||||
|
||||
it('should fail when output channel name is an empty string', () => {
|
||||
try {
|
||||
OutputUri.create('');
|
||||
fail('Expected failure.');
|
||||
} catch (e) {
|
||||
expect(e.message).to.be.equal("'name' must be defined.");
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail when output channel name contains whitespace only', () => {
|
||||
try {
|
||||
OutputUri.create(' \t');
|
||||
fail('Expected failure.');
|
||||
} catch (e) {
|
||||
expect(e.message).to.be.equal("'name' must contain at least one non-whitespace character.");
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle whitespace', () => {
|
||||
const uri = OutputUri.create('foo bar');
|
||||
const name = OutputUri.channelName(uri);
|
||||
expect(name).to.be.equal('foo bar');
|
||||
});
|
||||
|
||||
it('should handle special characters (:) gracefully', () => {
|
||||
const uri = OutputUri.create('foo: bar');
|
||||
const name = OutputUri.channelName(uri);
|
||||
expect(name).to.be.equal('foo: bar');
|
||||
});
|
||||
|
||||
});
|
||||
47
packages/output/src/common/output-uri.ts
Normal file
47
packages/output/src/common/output-uri.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export namespace OutputUri {
|
||||
|
||||
export const SCHEME = 'output';
|
||||
|
||||
export function is(uri: string | URI): boolean {
|
||||
if (uri instanceof URI) {
|
||||
return uri.scheme === SCHEME;
|
||||
}
|
||||
return is(new URI(uri));
|
||||
}
|
||||
|
||||
export function create(name: string): URI {
|
||||
if (!name) {
|
||||
throw new Error("'name' must be defined.");
|
||||
}
|
||||
if (!name.trim().length) {
|
||||
throw new Error("'name' must contain at least one non-whitespace character.");
|
||||
}
|
||||
return new URI(encodeURIComponent(name)).withScheme(SCHEME);
|
||||
}
|
||||
|
||||
export function channelName(uri: string | URI): string {
|
||||
if (!is(uri)) {
|
||||
throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`);
|
||||
}
|
||||
return (uri instanceof URI ? uri : new URI(uri)).toString(true).slice(`${OutputUri.SCHEME}:/`.length);
|
||||
}
|
||||
|
||||
}
|
||||
22
packages/output/src/node/output-backend-module.ts
Normal file
22
packages/output/src/node/output-backend-module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics 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
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { bindOutputPreferences } from '../common/output-preferences';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bindOutputPreferences(bind);
|
||||
});
|
||||
Reference in New Issue
Block a user