From 4d718464c4fc827e2d2f902caf18da60f487c3a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 10 Apr 2019 12:33:36 +0200 Subject: [PATCH] Rendering improvements --- .../terminal/browser/terminal.contribution.ts | 5 + .../terminal/browser/terminalInstance.ts | 13 ++ .../browser/terminalProcessManager.ts | 10 +- .../browser/terminalTypeAheadAddon.ts | 136 ++++++++++++++++++ .../contrib/terminal/common/terminal.ts | 10 ++ .../terminalInstanceService.ts | 2 + 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 91d20043872..fee04bfaf3d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -264,6 +264,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, + 'terminal.integrated.enableLatencyMitigation': { + description: nls.localize('terminal.integrated.enableLatencyMitigation', "Whether to enable the latency mitigation feature for high-latency terminals."), + type: 'boolean', + default: false + }, 'terminal.integrated.experimentalRefreshOnResume': { description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 52af0d5ba5a..735c23982e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -587,6 +587,19 @@ export class TerminalInstance implements ITerminalInstance { if (this._processManager) { this._widgetManager = new TerminalWidgetManager(this._wrapperElement); this._processManager.onProcessReady(() => this._linkHandler.setWidgetManager(this._widgetManager)); + + this._processManager.onProcessReady(() => { + if (this._configHelper.config.enableLatencyMitigation) { + if (!this._processManager) { + return; + } + this._processManager.getLatency().then(latency => { + if (latency > 20 && (this._xterm as any).typeAheadInit) { + (this._xterm as any).typeAheadInit(this._processManager, this._themeService); + } + }); + } + }); } const computedStyle = window.getComputedStyle(this._container); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 9fa87213da3..fe1cc18d5f2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -6,7 +6,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -56,6 +56,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { private readonly _onProcessReady = new Emitter(); public get onProcessReady(): Event { return this._onProcessReady.event; } + private readonly _onBeforeProcessData = new Emitter(); + public get onBeforeProcessData(): Event { return this._onBeforeProcessData.event; } private readonly _onProcessData = new Emitter(); public get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessTitle = new Emitter(); @@ -181,7 +183,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { const p = this._process!; p.onProcessData(data => { - this._onProcessData.fire(data); + const beforeProcessDataEvent: IBeforeProcessDataEvent = { data }; + this._onBeforeProcessData.fire(beforeProcessDataEvent); + if (beforeProcessDataEvent.data && beforeProcessDataEvent.data.length > 0) { + this._onProcessData.fire(beforeProcessDataEvent.data); + } }); p.onProcessIdReady(pid => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts new file mode 100644 index 00000000000..03eb7660199 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Terminal as XTermTerminal } from 'vscode-xterm'; +import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export interface ITerminalCore { + buffer: any; +} + +export interface ITypeAheadAddonTerminal { + _core: ITerminalCore; + on: any; + write(data: string): void; + cols: number; + + __typeAheadQueue: string[]; + __typeAheadState: TypeAheadState; + __typeAheadCurrentYBase: number; + __typeAheadCurrentY: number; +} + +enum TypeAheadState { + /** + * The normal state, ready to type if it starts. + */ + Normal, + /** + * Something happens such that we cannot make a good guess on what to print, + * wait until the cursor row changes before proceeding. + */ + AwaitingRowChange +} + +function isCharPrintable(data: string): boolean { + const code = data.charCodeAt(0); + return data.length === 1 && code >= 32 && code <= 126; +} + +function init(terminal: any, processManager: ITerminalProcessManager, themeService: IThemeService): void { + const t = terminal as ITypeAheadAddonTerminal; + + t.__typeAheadQueue = []; + t.__typeAheadState = TypeAheadState.Normal; + t.__typeAheadCurrentYBase = 0; + t.__typeAheadCurrentY = 0; + + function typeAhead(data: string): void { + for (let i = 0; i < data.length; i++) { + t.__typeAheadQueue.push(data[i]); + } + t.write(data); + } + + t.on('cursormove', () => { + // Reset if the cursor row changed + if (t._core.buffer.ybase !== t.__typeAheadCurrentYBase || t._core.buffer.y !== t.__typeAheadCurrentY) { + t.__typeAheadCurrentYBase = t._core.buffer.ybase; + t.__typeAheadCurrentY = t._core.buffer.y; + t.__typeAheadState = TypeAheadState.Normal; + } + }); + + t.on('data', (data: string) => { + // Exit if we're waiting for a row change + if (t.__typeAheadState === TypeAheadState.AwaitingRowChange) { + return; + } + + // Only enable in the normal buffer + if (!t._core.buffer._hasScrollback) { + return; + } + + // // Handle enter + // if (data === '\r') { + // typeAhead('\r\n'); + // return; + // } + + // // Left arrow + // if (data === '\x1b[D') { + // // TODO: How to stop it from going beyond prompt? + // typeAhead(String.fromCharCode(8)); + // } + + // // Right arrow + // if (data === '\x1b[C') { + // // TODO: How to stop it from going beyond prompt? + // typeAhead('\x1b[C'); + // } + + // // Backspace (DEL) + // if (data.charCodeAt(0) === 127) { + // // TODO: This would require knowing the prompt length to be able to shift everything + // } + + if (!isCharPrintable(data)) { + t.__typeAheadState = TypeAheadState.AwaitingRowChange; + return; + } + + if (t._core.buffer.x === t.cols - 1) { + // TODO: Does the space get added on Windows/Linux too? + data += ' \r'; + } + typeAhead(data); + }); + + processManager.onBeforeProcessData(event => { + let consumeCount = 0; + for (let i = 0; i < event.data.length; i++) { + if (t.__typeAheadQueue[0] === event.data[i]) { + t.__typeAheadQueue.shift(); + consumeCount++; + } else { + t.__typeAheadQueue.length = 0; + break; + } + } + if (consumeCount === event.data.length) { + event.data = ''; + } else if (consumeCount > 0) { + event.data = event.data.substr(consumeCount); + } + }); +} + +export function apply(terminalConstructor: typeof XTermTerminal) { + (terminalConstructor.prototype).typeAheadInit = function (processManager: ITerminalProcessManager, themeService: IThemeService): void { + init(this, processManager, themeService); + }; +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index c55a614d105..b4d9e485a14 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -101,6 +101,7 @@ export interface ITerminalConfiguration { experimentalBufferImpl: 'JsArray' | 'TypedArray'; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; + enableLatencyMitigation: boolean; experimentalRefreshOnResume: boolean; } @@ -634,6 +635,14 @@ export interface ITerminalCommandTracker { selectToNextLine(): void; } +export interface IBeforeProcessDataEvent { + /** + * The data of the event, this can be modified by the event listener to change what gets sent + * to the terminal. + */ + data: string; +} + export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; @@ -643,6 +652,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly userHome: string | undefined; readonly onProcessReady: Event; + readonly onBeforeProcessData: Event; readonly onProcessData: Event; readonly onProcessTitle: Event; readonly onProcessExit: Event; diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index 47acc177be8..9fedff9cb21 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -12,6 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; +import * as typeAheadAddon from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; let Terminal: typeof XTermTerminal; @@ -35,6 +36,7 @@ export class TerminalInstanceService implements ITerminalInstanceService { Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/search/search')); Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/webLinks/webLinks')); Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/winptyCompat/winptyCompat')); + Terminal.applyAddon(typeAheadAddon); // Localize strings Terminal.strings.blankLine = nls.localize('terminal.integrated.a11yBlankLine', 'Blank line'); Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); -- GitLab