@theia/console
Version:
Theia - Console Extension
352 lines (301 loc) • 12.9 kB
text/typescript
// *****************************************************************************
// 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 { ElementExt } from '@theia/core/shared/@lumino/domutils';
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
import { TreeSourceNode } from '@theia/core/lib/browser/source-tree';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
import { BaseWidget, PanelLayout, Widget, Message, MessageLoop, StatefulWidget, CompositeTreeNode } from '@theia/core/lib/browser';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ConsoleHistory } from './console-history';
import { ConsoleContentWidget } from './console-content-widget';
import { ConsoleSession } from './console-session';
import { ConsoleSessionManager } from './console-session-manager';
import * as monaco from '@theia/monaco-editor-core';
import { Disposable } from '@theia/core/lib/common/disposable';
import { EditorManager } from '@theia/editor/lib/browser';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
export const ConsoleOptions = Symbol('ConsoleWidgetOptions');
export interface ConsoleOptions {
id: string
title?: {
label?: string
iconClass?: string
caption?: string
}
input: {
uri: URI
options?: MonacoEditor.IOptions
}
inputFocusContextKey?: ContextKey<boolean>
}
export class ConsoleWidget extends BaseWidget implements StatefulWidget {
static styles = {
node: 'theia-console-widget',
content: 'theia-console-content',
input: 'theia-console-input',
};
static createContainer(parent: interfaces.Container, options: ConsoleOptions): Container {
const child = ConsoleContentWidget.createContainer(parent);
child.bind(ConsoleHistory).toSelf();
child.bind(ConsoleOptions).toConstantValue(options);
child.bind(ConsoleWidget).toSelf();
return child;
}
protected readonly options: ConsoleOptions;
readonly content: ConsoleContentWidget;
protected readonly history: ConsoleHistory;
protected readonly sessionManager: ConsoleSessionManager;
protected readonly editorProvider: MonacoEditorProvider;
protected readonly contextKeyService: ContextKeyService;
protected readonly editorService: MonacoEditorService;
protected readonly editorManager: EditorManager;
protected _input: MonacoEditor;
protected _inputFocusContextKey: ContextKey<boolean>;
protected modelChangeListener = Disposable.NULL;
protected _ready: Promise<void> | undefined;
get ready(): Promise<void> {
if (!this._ready) {
throw new Error('ready must not be accessed in the construction phase');
}
return this._ready;
}
constructor() {
super();
this.node.classList.add(ConsoleWidget.styles.node);
}
protected init(): void {
this._ready = this.doInit();
}
protected async doInit(): Promise<void> {
const { id, title, inputFocusContextKey } = this.options;
const { label, iconClass, caption } = Object.assign({}, title);
this.id = id;
this.title.closable = true;
this.title.label = label || id;
if (iconClass) {
this.title.iconClass = iconClass;
}
this.title.caption = caption || label || id;
const layout = this.layout = new PanelLayout();
this.content.node.classList.add(ConsoleWidget.styles.content);
this.toDispose.push(this.content);
layout.addWidget(this.content);
const inputWidget = new Widget();
inputWidget.node.classList.add(ConsoleWidget.styles.input);
layout.addWidget(inputWidget);
const input = this._input = await this.createInput(inputWidget.node);
this.toDispose.push(input);
this.toDispose.push(input.getControl().onDidLayoutChange(() => this.resizeContent()));
this.toDispose.push(input.getControl().onDidChangeConfiguration(event => {
if (event.hasChanged(monaco.editor.EditorOption.fontInfo)) {
this.updateFont();
}
}));
this.session = this.sessionManager.selectedSession;
this.toDispose.push(this.sessionManager.onDidChangeSelectedSession(session => {
// Do not clear the session output when `undefined`.
if (session) {
this.session = session;
}
}));
this.updateFont();
if (inputFocusContextKey) {
this.toDispose.push(input.onFocusChanged(() => inputFocusContextKey.set(this.hasInputFocus())));
this.toDispose.push(input.onCursorPositionChanged(() => input.getControl().createContextKey('consoleNavigationBackEnabled', this.consoleNavigationBackEnabled)));
this.toDispose.push(input.onCursorPositionChanged(() => input.getControl().createContextKey('consoleNavigationForwardEnabled', this.consoleNavigationForwardEnabled)));
}
input.getControl().createContextKey('consoleInputFocus', true);
const contentContext = this.contextKeyService.createScoped(this.content.node);
contentContext.setContext('consoleContentFocus', true);
this.toDispose.pushAll([
this.editorManager.onActiveEditorChanged(() => this.setMode()),
this.onDidChangeVisibility(() => this.setMode())
]);
}
protected createInput(node: HTMLElement): Promise<MonacoEditor> {
return this.editorProvider.createInline(this.options.input.uri, node, this.options.input.options);
}
protected updateFont(): void {
const { fontFamily, fontSize, lineHeight } = this._input.getControl().getOption(monaco.editor.EditorOption.fontInfo);
this.content.node.style.fontFamily = fontFamily;
this.content.node.style.fontSize = fontSize + 'px';
this.content.node.style.lineHeight = lineHeight + 'px';
}
protected _session: ConsoleSession | undefined;
set session(session: ConsoleSession | undefined) {
if (this._session === session) {
return;
}
this._session = session;
this.content.source = session;
}
get session(): ConsoleSession | undefined {
return this._session;
}
get input(): MonacoEditor {
return this._input;
}
get consoleNavigationBackEnabled(): boolean {
const editor = this.input.getControl();
return !!editor.getPosition()!.equals({ lineNumber: 1, column: 1 });
}
get consoleNavigationForwardEnabled(): boolean {
const editor = this.input.getControl();
const model = editor.getModel();
if (!model) {
return false;
}
const lineNumber = editor.getModel()!.getLineCount();
const column = editor.getModel()!.getLineMaxColumn(lineNumber);
return !!editor.getPosition()!.equals({ lineNumber, column });
}
selectAll(): void {
const selection = document.getSelection();
if (selection) {
selection.selectAllChildren(this.content.node);
}
}
collapseAll(): void {
const { root } = this.content.model;
if (CompositeTreeNode.is(root)) {
this.content.model.collapseAll(root);
}
}
clear(): void {
if (this.session) {
this.session.clear();
}
}
async execute(value?: string): Promise<void> {
if (value === undefined) {
value = this._input.getControl().getValue();
this._input.getControl().setValue('');
}
this.history.push(value);
if (this.session) {
const listener = this.content.model.onNodeRefreshed(() => {
listener.dispose();
this.revealLastOutput();
});
await this.session.execute(value);
}
}
navigateBack(): void {
const value = this.history.previous;
if (value === undefined) {
return;
}
const editor = this.input.getControl();
editor.setValue(value);
editor.setPosition({
lineNumber: 1,
column: 1
});
}
navigateForward(): void {
const value = this.history.next || '';
const editor = this.input.getControl();
editor.setValue(value);
const lineNumber = editor.getModel()!.getLineCount();
const column = editor.getModel()!.getLineMaxColumn(lineNumber);
editor.setPosition({ lineNumber, column });
}
protected revealLastOutput(): void {
const { root } = this.content.model;
if (TreeSourceNode.is(root)) {
this.content.model.selectNode(root.children[root.children.length - 1]);
}
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this._input.focus();
}
protected totalHeight = -1;
protected totalWidth = -1;
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.totalWidth = msg.width;
this.totalHeight = msg.height;
this._input.resizeToFit();
this.resizeContent();
}
protected resizeContent(): void {
this.totalHeight = this.totalHeight < 0 ? this.computeHeight() : this.totalHeight;
const inputHeight = this._input.getControl().getLayoutInfo().height;
const contentHeight = this.totalHeight - inputHeight;
this.content.node.style.height = `${contentHeight}px`;
MessageLoop.sendMessage(this.content, new Widget.ResizeMessage(this.totalWidth, contentHeight));
}
protected computeHeight(): number {
const { verticalSum } = ElementExt.boxSizing(this.node);
return this.node.offsetHeight - verticalSum;
}
storeState(): object {
const history = this.history.store();
const input = this.input.storeViewState();
return {
history,
input
};
}
restoreState(oldState: object): void {
if ('history' in oldState) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.history.restore((<any>oldState)['history']);
}
this.input.getControl().setValue(this.history.current || '');
if ('input' in oldState) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.input.restoreViewState((<any>oldState)['input']);
}
}
hasInputFocus(): boolean {
return this._input && this._input.isFocused({ strict: true });
}
override dispose(): void {
super.dispose();
this.modelChangeListener.dispose();
}
// To set the active language for the console input text model.
// https://github.com/microsoft/vscode/blob/2af422737386e792c3fcde7884f9bf47a1aff2f5/src/vs/workbench/contrib/debug/browser/repl.ts#L371-L384
protected setMode(): void {
if (this.isHidden) {
return;
}
const activeEditorControl = this.editorService.getActiveCodeEditor();
if (activeEditorControl) {
this.modelChangeListener.dispose();
this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode());
const consoleModel = this._input.getControl().getModel();
const activeEditorModel = activeEditorControl.getModel();
if (consoleModel && activeEditorModel) {
monaco.editor.setModelLanguage(consoleModel, activeEditorModel.getLanguageId());
}
}
}
}