Merge pull request #129207 from microsoft/tyriar/116113
xterm.js node target and serialize addon
This commit is contained in:
commit
3bec1313ae
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.60.0",
|
||||
"distro": "8a46e44036ace8089e2e1bc6c1af11dad15825b1",
|
||||
"distro": "20426c9ab21f26752e1640818c3a5d148863c78e",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
@ -88,6 +88,7 @@
|
|||
"xterm-addon-search": "0.9.0-beta.3",
|
||||
"xterm-addon-unicode11": "0.3.0-beta.5",
|
||||
"xterm-addon-webgl": "0.12.0-beta.5",
|
||||
"xterm-headless": "4.14.0-beta.3",
|
||||
"yauzl": "^2.9.2",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"xterm-addon-search": "0.9.0-beta.3",
|
||||
"xterm-addon-unicode11": "0.3.0-beta.5",
|
||||
"xterm-addon-webgl": "0.12.0-beta.5",
|
||||
"xterm-headless": "4.14.0-beta.3",
|
||||
"yauzl": "^2.9.2",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
|
|
|
@ -635,6 +635,11 @@ xterm-addon-webgl@0.12.0-beta.5:
|
|||
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.5.tgz#97524d1a455d15096c1999b8ce73ab0c6ecc8efa"
|
||||
integrity sha512-Od9yFikC414E4Fm1W8akhSiNDUWat+RLsEYJMwvcEW4HprUVJije6n2Lrzj4roNl4pnSPUhF89zG6NY4lZXqDg==
|
||||
|
||||
xterm-headless@4.14.0-beta.3:
|
||||
version "4.14.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.3.tgz#c737c1c700cc3dd6207294d9c233acf1676970da"
|
||||
integrity sha512-DVzUVTJBHQ906p08fsLkVyWA+UC7faKaudgjmGpMOcJjQTXNGmcQQhpoUq8mYCukD4ONjmJ9UOq5snBTASR+mw==
|
||||
|
||||
xterm@4.14.0-beta.6:
|
||||
version "4.14.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.6.tgz#f39ec3298059dbfe09f8f5429ad55bc5472fdc4a"
|
||||
|
|
|
@ -68,7 +68,7 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'
|
|||
import { combinedAppender, ITelemetryAppender, NullAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/customEndpointTelemetryService';
|
||||
import { LocalReconnectConstants, TerminalIpcChannels } from 'vs/platform/terminal/common/terminal';
|
||||
import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
|
||||
import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal';
|
||||
import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService';
|
||||
import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync';
|
||||
|
@ -276,8 +276,10 @@ class SharedProcessMain extends Disposable {
|
|||
ILocalPtyService,
|
||||
this._register(
|
||||
new PtyHostService({
|
||||
GraceTime: LocalReconnectConstants.GraceTime,
|
||||
ShortGraceTime: LocalReconnectConstants.ShortGraceTime
|
||||
graceTime: LocalReconnectConstants.GraceTime,
|
||||
shortGraceTime: LocalReconnectConstants.ShortGraceTime,
|
||||
scrollback: configurationService.getValue<number>(TerminalSettingId.PersistentSessionScrollback) || 1,
|
||||
useExperimentalSerialization: !!configurationService.getValue<boolean>(TerminalSettingId.PersistentSessionExperimentalSerializer),
|
||||
},
|
||||
configurationService,
|
||||
logService,
|
||||
|
|
|
@ -89,6 +89,8 @@ export const enum TerminalSettingId {
|
|||
LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms',
|
||||
LocalEchoStyle = 'terminal.integrated.localEchoStyle',
|
||||
EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions',
|
||||
PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback',
|
||||
PersistentSessionExperimentalSerializer = 'terminal.integrated.persistentSessionExperimentalSerializer',
|
||||
InheritEnv = 'terminal.integrated.inheritEnv',
|
||||
ShowLinkHover = 'terminal.integrated.showLinkHover',
|
||||
}
|
||||
|
@ -489,8 +491,10 @@ export interface ITerminalChildProcess {
|
|||
}
|
||||
|
||||
export interface IReconnectConstants {
|
||||
GraceTime: number,
|
||||
ShortGraceTime: number
|
||||
graceTime: number;
|
||||
shortGraceTime: number;
|
||||
scrollback: number;
|
||||
useExperimentalSerialization: boolean;
|
||||
}
|
||||
|
||||
export const enum LocalReconnectConstants {
|
||||
|
|
|
@ -360,6 +360,18 @@ const terminalPlatformConfiguration: IConfigurationNode = {
|
|||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
[TerminalSettingId.PersistentSessionScrollback]: {
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
markdownDescription: localize('terminal.integrated.persistentSessionScrollback', "Controls the maximum amount of lines that will be restored when reconnecting to a persistent terminal session. Increasing this will restore more lines of scrollback at the cost of more memory and increase the time it takes to connect to terminals on start up. This setting requires a restart to take effect and should be set to a value less than or equal to `#terminal.integrated.scrollback#`."),
|
||||
type: 'number',
|
||||
default: 100
|
||||
},
|
||||
[TerminalSettingId.PersistentSessionExperimentalSerializer]: {
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('terminal.integrated.persistentSessionExperimentalSerializer', "Whether to use a more efficient experimental approach for restoring the terminal's buffer. This setting requires a restart to take effect."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
[TerminalSettingId.ShowLinkHover]: {
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('terminal.integrated.showLinkHover', "Whether to show hovers for links in the terminal output."),
|
||||
|
|
|
@ -17,7 +17,13 @@ export interface IRemoteTerminalProcessReplayEvent {
|
|||
events: ReplayEntry[];
|
||||
}
|
||||
|
||||
export class TerminalRecorder {
|
||||
export interface ITerminalSerializer {
|
||||
handleData(data: string): void;
|
||||
handleResize(cols: number, rows: number): void;
|
||||
generateReplayEvent(): IPtyHostProcessReplayEvent;
|
||||
}
|
||||
|
||||
export class TerminalRecorder implements ITerminalSerializer {
|
||||
|
||||
private _entries: RecorderEntry[];
|
||||
private _totalDataLength: number = 0;
|
||||
|
@ -26,7 +32,7 @@ export class TerminalRecorder {
|
|||
this._entries = [{ cols, rows, data: [] }];
|
||||
}
|
||||
|
||||
recordResize(cols: number, rows: number): void {
|
||||
handleResize(cols: number, rows: number): void {
|
||||
if (this._entries.length > 0) {
|
||||
const lastEntry = this._entries[this._entries.length - 1];
|
||||
if (lastEntry.data.length === 0) {
|
||||
|
@ -52,7 +58,7 @@ export class TerminalRecorder {
|
|||
this._entries.push({ cols, rows, data: [] });
|
||||
}
|
||||
|
||||
recordData(data: string): void {
|
||||
handleData(data: string): void {
|
||||
const lastEntry = this._entries[this._entries.length - 1];
|
||||
lastEntry.data.push(data);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
|||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { ConsoleLogger, LogService } from 'vs/platform/log/common/log';
|
||||
import { LogLevelChannel } from 'vs/platform/log/common/logIpc';
|
||||
import { TerminalIpcChannels } from 'vs/platform/terminal/common/terminal';
|
||||
import { IReconnectConstants, TerminalIpcChannels } from 'vs/platform/terminal/common/terminal';
|
||||
import { HeartbeatService } from 'vs/platform/terminal/node/heartbeatService';
|
||||
import { PtyService } from 'vs/platform/terminal/node/ptyService';
|
||||
|
||||
|
@ -23,9 +23,16 @@ server.registerChannel(TerminalIpcChannels.Log, logChannel);
|
|||
const heartbeatService = new HeartbeatService();
|
||||
server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService));
|
||||
|
||||
const reconnectConstants = { GraceTime: parseInt(process.env.VSCODE_RECONNECT_GRACE_TIME || '0'), ShortGraceTime: parseInt(process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME || '0') };
|
||||
const reconnectConstants: IReconnectConstants = {
|
||||
graceTime: parseInt(process.env.VSCODE_RECONNECT_GRACE_TIME || '0'),
|
||||
shortGraceTime: parseInt(process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME || '0'),
|
||||
scrollback: parseInt(process.env.VSCODE_RECONNECT_SCROLLBACK || '100'),
|
||||
useExperimentalSerialization: !!parseInt(process.env.VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION || '1')
|
||||
};
|
||||
delete process.env.VSCODE_RECONNECT_GRACE_TIME;
|
||||
delete process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME;
|
||||
delete process.env.VSCODE_RECONNECT_SCROLLBACK;
|
||||
delete process.env.VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION;
|
||||
|
||||
const ptyService = new PtyService(lastPtyId, logService, reconnectConstants);
|
||||
server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService));
|
||||
|
|
|
@ -107,6 +107,7 @@ export class PtyHostService extends Disposable implements IPtyService {
|
|||
}
|
||||
|
||||
private _startPtyHost(): [Client, IPtyService] {
|
||||
this._logService.info('use experimental serialize ' + (this._reconnectConstants.useExperimentalSerialization ? 1 : 0));
|
||||
const client = new Client(
|
||||
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
|
||||
{
|
||||
|
@ -117,8 +118,10 @@ export class PtyHostService extends Disposable implements IPtyService {
|
|||
VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain',
|
||||
VSCODE_PIPE_LOGGING: 'true',
|
||||
VSCODE_VERBOSE_LOGGING: 'true', // transmit console logs from server to client,
|
||||
VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.GraceTime,
|
||||
VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.ShortGraceTime
|
||||
VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.graceTime,
|
||||
VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.shortGraceTime,
|
||||
VSCODE_RECONNECT_SCROLLBACK: this._reconnectConstants.scrollback,
|
||||
VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION: this._reconnectConstants.useExperimentalSerialization ? 1 : 0
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -15,8 +15,10 @@ import { RequestStore } from 'vs/platform/terminal/common/requestStore';
|
|||
import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal';
|
||||
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
|
||||
import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment';
|
||||
import { Terminal as XtermTerminal } from 'xterm-headless';
|
||||
import { SerializeAddon } from 'vs/platform/terminal/node/serializeAddon';
|
||||
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess';
|
||||
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
|
||||
import { ITerminalSerializer, TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
|
||||
import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment';
|
||||
import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess';
|
||||
|
||||
|
@ -307,7 +309,6 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
|
||||
private readonly _pendingCommands = new Map<number, { resolve: (data: any) => void; reject: (err: any) => void; }>();
|
||||
|
||||
private readonly _recorder: TerminalRecorder;
|
||||
private _isStarted: boolean = false;
|
||||
|
||||
private _orphanQuestionBarrier: AutoOpenBarrier | null;
|
||||
|
@ -337,6 +338,7 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
private _cwd = '';
|
||||
private _title: string | undefined;
|
||||
private _titleSource: TitleEventSource = TitleEventSource.Process;
|
||||
private _serializer: ITerminalSerializer;
|
||||
|
||||
get pid(): number { return this._pid; }
|
||||
get title(): string { return this._title || this._terminalProcess.currentTitle; }
|
||||
|
@ -360,7 +362,8 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
readonly workspaceId: string,
|
||||
readonly workspaceName: string,
|
||||
readonly shouldPersistTerminal: boolean,
|
||||
cols: number, rows: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
reconnectConstants: IReconnectConstants,
|
||||
private readonly _logService: ILogService,
|
||||
private _icon?: TerminalIcon,
|
||||
|
@ -368,17 +371,26 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
) {
|
||||
super();
|
||||
this._logService.trace('persistentTerminalProcess#ctor', _persistentProcessId, arguments);
|
||||
this._recorder = new TerminalRecorder(cols, rows);
|
||||
|
||||
if (reconnectConstants.useExperimentalSerialization) {
|
||||
this._serializer = new XtermSerializer(
|
||||
cols,
|
||||
rows,
|
||||
reconnectConstants.scrollback
|
||||
);
|
||||
} else {
|
||||
this._serializer = new TerminalRecorder(cols, rows);
|
||||
}
|
||||
this._orphanQuestionBarrier = null;
|
||||
this._orphanQuestionReplyTime = 0;
|
||||
this._disconnectRunner1 = this._register(new RunOnceScheduler(() => {
|
||||
this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(reconnectConstants.GraceTime)} has expired, shutting down pid "${this._pid}"`);
|
||||
this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(reconnectConstants.graceTime)} has expired, shutting down pid "${this._pid}"`);
|
||||
this.shutdown(true);
|
||||
}, reconnectConstants.GraceTime));
|
||||
}, reconnectConstants.graceTime));
|
||||
this._disconnectRunner2 = this._register(new RunOnceScheduler(() => {
|
||||
this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(reconnectConstants.ShortGraceTime)} has expired, shutting down pid ${this._pid}`);
|
||||
this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(reconnectConstants.shortGraceTime)} has expired, shutting down pid ${this._pid}`);
|
||||
this.shutdown(true);
|
||||
}, reconnectConstants.ShortGraceTime));
|
||||
}, reconnectConstants.shortGraceTime));
|
||||
|
||||
this._register(this._terminalProcess.onProcessReady(e => {
|
||||
this._pid = e.pid;
|
||||
|
@ -394,7 +406,7 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId)));
|
||||
|
||||
// Data recording for reconnect
|
||||
this._register(this.onProcessData(e => this._recorder.recordData(e)));
|
||||
this._register(this.onProcessData(e => this._serializer.handleData(e)));
|
||||
}
|
||||
|
||||
attach(): void {
|
||||
|
@ -445,7 +457,7 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
if (this._inReplay) {
|
||||
return;
|
||||
}
|
||||
this._recorder.recordResize(cols, rows);
|
||||
this._serializer.handleResize(cols, rows);
|
||||
|
||||
// Buffered events should flush when a resize occurs
|
||||
this._bufferer.flushBuffer(this._persistentProcessId);
|
||||
|
@ -468,12 +480,11 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
}
|
||||
|
||||
triggerReplay(): void {
|
||||
const ev = this._recorder.generateReplayEvent();
|
||||
const ev = this._serializer.generateReplayEvent();
|
||||
let dataLength = 0;
|
||||
for (const e of ev.events) {
|
||||
dataLength += e.data.length;
|
||||
}
|
||||
|
||||
this._logService.info(`Persistent process "${this._persistentProcessId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
|
||||
this._onProcessReplay.fire(ev);
|
||||
this._terminalProcess.clearUnacknowledgedChars();
|
||||
|
@ -530,6 +541,36 @@ export class PersistentTerminalProcess extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
class XtermSerializer implements ITerminalSerializer {
|
||||
private _xterm: XtermTerminal;
|
||||
constructor(cols: number, rows: number, scrollback: number) {
|
||||
this._xterm = new XtermTerminal({ cols, rows, scrollback });
|
||||
}
|
||||
|
||||
handleData(data: string): void {
|
||||
this._xterm.write(data);
|
||||
}
|
||||
|
||||
handleResize(cols: number, rows: number): void {
|
||||
this._xterm.resize(cols, rows);
|
||||
}
|
||||
|
||||
generateReplayEvent(): IPtyHostProcessReplayEvent {
|
||||
const serialize = new SerializeAddon();
|
||||
this._xterm.loadAddon(serialize);
|
||||
const serialized = serialize.serialize(this._xterm.getOption('scrollback'));
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
cols: this._xterm.getOption('cols'),
|
||||
rows: this._xterm.getOption('rows'),
|
||||
data: serialized
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function printTime(ms: number): string {
|
||||
let h = 0;
|
||||
let m = 0;
|
||||
|
|
431
src/vs/platform/terminal/node/serializeAddon.ts
Normal file
431
src/vs/platform/terminal/node/serializeAddon.ts
Normal file
|
@ -0,0 +1,431 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* (EXPERIMENTAL) This Addon is still under development
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
}
|
||||
|
||||
// TODO: Refine this template class later
|
||||
abstract class BaseSerializeHandler {
|
||||
constructor(private _buffer: IBuffer) { }
|
||||
|
||||
public serialize(startRow: number, endRow: number): string {
|
||||
// we need two of them to flip between old and new cell
|
||||
const cell1 = this._buffer.getNullCell();
|
||||
const cell2 = this._buffer.getNullCell();
|
||||
let oldCell = cell1;
|
||||
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line) {
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
|
||||
if (!c) {
|
||||
console.warn(`Can't get cell at row=${row}, col=${col}`);
|
||||
continue;
|
||||
}
|
||||
this._nextCell(c, oldCell, row, col);
|
||||
oldCell = c;
|
||||
}
|
||||
}
|
||||
this._rowEnd(row, row === endRow - 1);
|
||||
}
|
||||
|
||||
this._afterSerialize();
|
||||
|
||||
return this._serializeString();
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||
protected _afterSerialize(): void { }
|
||||
protected _serializeString(): string { return ''; }
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return cell1.getFgColorMode() === cell2.getFgColorMode()
|
||||
&& cell1.getFgColor() === cell2.getFgColor();
|
||||
}
|
||||
|
||||
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return cell1.getBgColorMode() === cell2.getBgColorMode()
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
&& cell1.isItalic() === cell2.isItalic()
|
||||
&& cell1.isDim() === cell2.isDim();
|
||||
}
|
||||
|
||||
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0;
|
||||
private _allRows: string[] = new Array<string>();
|
||||
private _allRowSeparators: string[] = new Array<string>();
|
||||
private _currentRow: string = '';
|
||||
private _nullCellCount: number = 0;
|
||||
|
||||
// we can see a full colored cell and a null cell that only have background the same style
|
||||
// but the information isn't preserved by null cell itself
|
||||
// so wee need to record it when required.
|
||||
private _cursorStyle: IBufferCell = this._buffer1.getNullCell();
|
||||
|
||||
// where exact the cursor styles comes from
|
||||
// because we can't copy the cell directly
|
||||
// so we remember where the content comes from instead
|
||||
private _cursorStyleRow: number = 0;
|
||||
private _cursorStyleCol: number = 0;
|
||||
|
||||
// this is a null cell for reference for checking whether background is empty or not
|
||||
private _backgroundCell: IBufferCell = this._buffer1.getNullCell();
|
||||
|
||||
private _firstRow: number = 0;
|
||||
private _lastCursorRow: number = 0;
|
||||
private _lastCursorCol: number = 0;
|
||||
private _lastContentCursorRow: number = 0;
|
||||
private _lastContentCursorCol: number = 0;
|
||||
|
||||
constructor(private _buffer1: IBuffer, private _terminal: Terminal) {
|
||||
super(_buffer1);
|
||||
}
|
||||
|
||||
protected override _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._allRows = new Array<string>(rows);
|
||||
this._lastContentCursorRow = start;
|
||||
this._lastCursorRow = start;
|
||||
this._firstRow = start;
|
||||
}
|
||||
|
||||
private _thisRowLastChar: IBufferCell = this._buffer1.getNullCell();
|
||||
private _thisRowLastSecondChar: IBufferCell = this._buffer1.getNullCell();
|
||||
private _nextRowFirstChar: IBufferCell = this._buffer1.getNullCell();
|
||||
protected override _rowEnd(row: number, isLastRow: boolean): void {
|
||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// use clear right to set background.
|
||||
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||
}
|
||||
|
||||
let rowSeparator = '';
|
||||
|
||||
// handle row separator
|
||||
if (!isLastRow) {
|
||||
// Enable BCE
|
||||
if (row - this._firstRow >= this._terminal.rows) {
|
||||
this._buffer1.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);
|
||||
}
|
||||
|
||||
// Fetch current line
|
||||
const currentLine = this._buffer1.getLine(row)!;
|
||||
// Fetch next line
|
||||
const nextLine = this._buffer1.getLine(row + 1)!;
|
||||
|
||||
if (!nextLine.isWrapped) {
|
||||
// just insert the line break
|
||||
rowSeparator = '\r\n';
|
||||
// we sended the enter
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
} else {
|
||||
rowSeparator = '';
|
||||
const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;
|
||||
const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;
|
||||
const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;
|
||||
const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;
|
||||
|
||||
// validate whether this line wrap is ever possible
|
||||
// which mean whether cursor can placed at a overflow position (x === row) naturally
|
||||
let isValid = false;
|
||||
|
||||
if (
|
||||
// you must output character to cause overflow, control sequence can't do this
|
||||
nextRowFirstChar.getChars() &&
|
||||
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
|
||||
) {
|
||||
if (
|
||||
// the last character can't be null,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (
|
||||
// the second to last character can't be null if the next line starts with CJK,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
isNextRowFirstCharDoubleWidth &&
|
||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
// force the wrap with magic
|
||||
// insert enough character to force the wrap
|
||||
rowSeparator = '-'.repeat(this._nullCellCount + 1);
|
||||
// move back and erase next line head
|
||||
rowSeparator += '\x1b[1D\x1b[1X';
|
||||
|
||||
if (this._nullCellCount > 0) {
|
||||
// do these because we filled the last several null slot, which we shouldn't
|
||||
rowSeparator += '\x1b[A';
|
||||
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}C`;
|
||||
rowSeparator += `\x1b[${this._nullCellCount}X`;
|
||||
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`;
|
||||
rowSeparator += '\x1b[B';
|
||||
}
|
||||
|
||||
// This is content and need the be serialized even it is invisible.
|
||||
// without this, wrap will be missing from outputs.
|
||||
this._lastContentCursorRow = row + 1;
|
||||
this._lastContentCursorCol = 0;
|
||||
|
||||
// force commit the cursor position
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow;
|
||||
this._allRowSeparators[this._rowIndex++] = rowSeparator;
|
||||
this._currentRow = '';
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
if (cell.isAttributeDefault()) {
|
||||
if (!oldCell.isAttributeDefault()) {
|
||||
sgrSeq.push(0);
|
||||
}
|
||||
} else {
|
||||
if (fgChanged) {
|
||||
const color = cell.getFgColor();
|
||||
if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||
else if (cell.isFgPalette()) {
|
||||
if (color >= 16) { sgrSeq.push(38, 5, color); }
|
||||
else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }
|
||||
}
|
||||
else { sgrSeq.push(39); }
|
||||
}
|
||||
if (bgChanged) {
|
||||
const color = cell.getBgColor();
|
||||
if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||
else if (cell.isBgPalette()) {
|
||||
if (color >= 16) { sgrSeq.push(48, 5, color); }
|
||||
else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }
|
||||
}
|
||||
else { sgrSeq.push(49); }
|
||||
}
|
||||
if (flagsChanged) {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
|
||||
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
|
||||
if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sgrSeq;
|
||||
}
|
||||
|
||||
protected override _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
|
||||
|
||||
// the empty cell style is only assumed to be changed when background changed, because foreground is always 0.
|
||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
|
||||
|
||||
/**
|
||||
* handles style change
|
||||
*/
|
||||
if (styleChanged) {
|
||||
// before update the style, we need to fill empty cell back
|
||||
if (this._nullCellCount > 0) {
|
||||
// use clear right to set background.
|
||||
if (!equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||
}
|
||||
// use move right to move cursor.
|
||||
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col;
|
||||
|
||||
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
|
||||
|
||||
// update the last cursor style
|
||||
const line = this._buffer1.getLine(row);
|
||||
if (line !== undefined) {
|
||||
line.getCell(col, this._cursorStyle);
|
||||
this._cursorStyleRow = row;
|
||||
this._cursorStyleCol = col;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles actual content
|
||||
*/
|
||||
if (isEmptyCell) {
|
||||
this._nullCellCount += cell.getWidth();
|
||||
} else {
|
||||
if (this._nullCellCount > 0) {
|
||||
// we can just assume we have same style with previous one here
|
||||
// because style change is handled by previous stage
|
||||
// use move right when background is empty, use clear right when there is background.
|
||||
if (equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||
} else {
|
||||
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||
}
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._currentRow += cell.getChars();
|
||||
|
||||
// update cursor
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
protected override _serializeString(): string {
|
||||
let rowEnd = this._allRows.length;
|
||||
|
||||
// the fixup is only required for data without scrollback
|
||||
// because it will always be placed at last line otherwise
|
||||
if (this._buffer1.length - this._firstRow <= this._terminal.rows) {
|
||||
rowEnd = this._lastContentCursorRow + 1 - this._firstRow;
|
||||
this._lastCursorCol = this._lastContentCursorCol;
|
||||
this._lastCursorRow = this._lastContentCursorRow;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
for (let i = 0; i < rowEnd; i++) {
|
||||
content += this._allRows[i];
|
||||
if (i + 1 < rowEnd) {
|
||||
content += this._allRowSeparators[i];
|
||||
}
|
||||
}
|
||||
|
||||
// restore the cursor
|
||||
const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY;
|
||||
const realCursorCol = this._buffer1.cursorX;
|
||||
|
||||
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||
|
||||
const moveRight = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}C`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}D`;
|
||||
}
|
||||
};
|
||||
const moveDown = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}B`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}A`;
|
||||
}
|
||||
};
|
||||
|
||||
if (cursorMoved) {
|
||||
moveDown(realCursorRow - this._lastCursorRow);
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializeAddon implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
private _getString(buffer: IBuffer, scrollback?: number): string {
|
||||
const maxRows = buffer.length;
|
||||
const handler = new StringSerializeHandler(buffer, this._terminal!);
|
||||
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + this!._terminal!.rows, 0, maxRows);
|
||||
const result = handler.serialize(maxRows - correctRows, maxRows);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public serialize(scrollback?: number): string {
|
||||
// TODO: Add combinedData support
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
if (this._terminal.buffer.active.type === 'normal') {
|
||||
return this._getString(this._terminal.buffer.active, scrollback);
|
||||
}
|
||||
|
||||
const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback);
|
||||
// alt screen don't have scrollback
|
||||
const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined);
|
||||
|
||||
return normalScreenContent
|
||||
+ '\u001b[?1049h\u001b[H'
|
||||
+ alternativeScreenContent;
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
}
|
|
@ -20,8 +20,8 @@ suite('TerminalRecorder', () => {
|
|||
eventsEqual(recorder, [
|
||||
{ cols: 1, rows: 2, data: '' }
|
||||
]);
|
||||
recorder.recordData('a');
|
||||
recorder.recordResize(3, 4);
|
||||
recorder.handleData('a');
|
||||
recorder.handleResize(3, 4);
|
||||
eventsEqual(recorder, [
|
||||
{ cols: 1, rows: 2, data: 'a' },
|
||||
{ cols: 3, rows: 4, data: '' }
|
||||
|
@ -32,18 +32,18 @@ suite('TerminalRecorder', () => {
|
|||
eventsEqual(recorder, [
|
||||
{ cols: 1, rows: 2, data: '' }
|
||||
]);
|
||||
recorder.recordResize(3, 4);
|
||||
recorder.handleResize(3, 4);
|
||||
eventsEqual(recorder, [
|
||||
{ cols: 3, rows: 4, data: '' }
|
||||
]);
|
||||
});
|
||||
test('should record data and combine it into the previous resize event', () => {
|
||||
const recorder = new TerminalRecorder(1, 2);
|
||||
recorder.recordData('a');
|
||||
recorder.recordData('b');
|
||||
recorder.recordResize(3, 4);
|
||||
recorder.recordData('c');
|
||||
recorder.recordData('d');
|
||||
recorder.handleData('a');
|
||||
recorder.handleData('b');
|
||||
recorder.handleResize(3, 4);
|
||||
recorder.handleData('c');
|
||||
recorder.handleData('d');
|
||||
eventsEqual(recorder, [
|
||||
{ cols: 1, rows: 2, data: 'ab' },
|
||||
{ cols: 3, rows: 4, data: 'cd' }
|
||||
|
|
|
@ -751,7 +751,7 @@ class SeamlessRelaunchDataFilter extends Disposable {
|
|||
|
||||
private _createRecorder(process: ITerminalChildProcess): [TerminalRecorder, IDisposable] {
|
||||
const recorder = new TerminalRecorder(0, 0);
|
||||
const disposable = process.onProcessData(e => recorder.recordData(typeof e === 'string' ? e : e.data));
|
||||
const disposable = process.onProcessData(e => recorder.handleData(typeof e === 'string' ? e : e.data));
|
||||
return [recorder, disposable];
|
||||
}
|
||||
|
||||
|
|
|
@ -11112,6 +11112,11 @@ xterm-addon-webgl@0.12.0-beta.5:
|
|||
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.5.tgz#97524d1a455d15096c1999b8ce73ab0c6ecc8efa"
|
||||
integrity sha512-Od9yFikC414E4Fm1W8akhSiNDUWat+RLsEYJMwvcEW4HprUVJije6n2Lrzj4roNl4pnSPUhF89zG6NY4lZXqDg==
|
||||
|
||||
xterm-headless@4.14.0-beta.3:
|
||||
version "4.14.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.3.tgz#c737c1c700cc3dd6207294d9c233acf1676970da"
|
||||
integrity sha512-DVzUVTJBHQ906p08fsLkVyWA+UC7faKaudgjmGpMOcJjQTXNGmcQQhpoUq8mYCukD4ONjmJ9UOq5snBTASR+mw==
|
||||
|
||||
xterm@4.14.0-beta.6:
|
||||
version "4.14.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.6.tgz#f39ec3298059dbfe09f8f5429ad55bc5472fdc4a"
|
||||
|
|
Loading…
Reference in a new issue