UNPKG

@theia/console

Version:
352 lines (301 loc) • 12.9 kB
// ***************************************************************************** // 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> } @injectable() 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; } @inject(ConsoleOptions) protected readonly options: ConsoleOptions; @inject(ConsoleContentWidget) readonly content: ConsoleContentWidget; @inject(ConsoleHistory) protected readonly history: ConsoleHistory; @inject(ConsoleSessionManager) protected readonly sessionManager: ConsoleSessionManager; @inject(MonacoEditorProvider) protected readonly editorProvider: MonacoEditorProvider; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; @inject(MonacoEditorService) protected readonly editorService: MonacoEditorService; @inject(EditorManager) 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); } @postConstruct() 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()); } } } }