deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View 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.
}
}

View 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',
};
}

View 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;
}
}

View 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;
}
}

View 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];
}
}

View 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
}
}

View 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);
});

View 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();
}
}

View 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();
}
};
}

View 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'] = () => { };

View 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;
}

View 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);
}

View 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');
});
});

View 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);
}
}

View 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);
});