Skip to content

Commit 3a90216

Browse files
committed
Sync unicode version with persistent terminal processes
Fixes #116113
1 parent f7e4c0a commit 3a90216

19 files changed

+155
-35
lines changed

src/vs/platform/terminal/common/terminal.ts

+8
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export interface IPtyService {
200200
cwd: string,
201201
cols: number,
202202
rows: number,
203+
unicodeVersion: '6' | '11',
203204
env: IProcessEnvironment,
204205
executableEnv: IProcessEnvironment,
205206
windowsEnableConpty: boolean,
@@ -223,6 +224,7 @@ export interface IPtyService {
223224
getCwd(id: number): Promise<string>;
224225
getLatency(id: number): Promise<number>;
225226
acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
227+
setUnicodeVersion(id: number, version: '6' | '11'): Promise<void>;
226228
processBinary(id: number, data: string): Promise<void>;
227229
/** Confirm the process is _not_ an orphan. */
228230
orphanQuestionReply(id: number): Promise<void>;
@@ -485,6 +487,12 @@ export interface ITerminalChildProcess {
485487
*/
486488
acknowledgeDataEvent(charCount: number): void;
487489

490+
/**
491+
* Sets the unicode version for the process, this drives the size of some characters in the
492+
* xterm-headless instance.
493+
*/
494+
setUnicodeVersion(version: '6' | '11'): Promise<void>;
495+
488496
getInitialCwd(): Promise<string>;
489497
getCwd(): Promise<string>;
490498
getLatency(): Promise<number>;

src/vs/platform/terminal/common/terminalRecorder.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export interface IRemoteTerminalProcessReplayEvent {
2020
export interface ITerminalSerializer {
2121
handleData(data: string): void;
2222
handleResize(cols: number, rows: number): void;
23-
generateReplayEvent(): IPtyHostProcessReplayEvent;
23+
generateReplayEvent(): Promise<IPtyHostProcessReplayEvent>;
24+
setUnicodeVersion?(version: '6' | '11'): void;
2425
}
2526

2627
export class TerminalRecorder implements ITerminalSerializer {
@@ -82,7 +83,7 @@ export class TerminalRecorder implements ITerminalSerializer {
8283
}
8384
}
8485

85-
generateReplayEvent(): IPtyHostProcessReplayEvent {
86+
generateReplayEventSync(): IPtyHostProcessReplayEvent {
8687
// normalize entries to one element per data array
8788
this._entries.forEach((entry) => {
8889
if (entry.data.length > 0) {
@@ -93,4 +94,8 @@ export class TerminalRecorder implements ITerminalSerializer {
9394
events: this._entries.map(entry => ({ cols: entry.cols, rows: entry.rows, data: entry.data[0] ?? '' }))
9495
};
9596
}
97+
98+
async generateReplayEvent(): Promise<IPtyHostProcessReplayEvent> {
99+
return this.generateReplayEventSync();
100+
}
96101
}

src/vs/platform/terminal/node/ptyHostService.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ export class PtyHostService extends Disposable implements IPtyService {
178178
super.dispose();
179179
}
180180

181-
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean, workspaceId: string, workspaceName: string): Promise<number> {
181+
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean, workspaceId: string, workspaceName: string): Promise<number> {
182182
const timeout = setTimeout(() => this._handleUnresponsiveCreateProcess(), HeartbeatConstants.CreateProcessTimeout);
183-
const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, shouldPersist, workspaceId, workspaceName);
183+
const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, windowsEnableConpty, shouldPersist, workspaceId, workspaceName);
184184
clearTimeout(timeout);
185185
lastPtyId = Math.max(lastPtyId, id);
186186
return id;
@@ -221,6 +221,9 @@ export class PtyHostService extends Disposable implements IPtyService {
221221
acknowledgeDataEvent(id: number, charCount: number): Promise<void> {
222222
return this._proxy.acknowledgeDataEvent(id, charCount);
223223
}
224+
setUnicodeVersion(id: number, version: '6' | '11'): Promise<void> {
225+
return this._proxy.setUnicodeVersion(id, version);
226+
}
224227
getInitialCwd(id: number): Promise<string> {
225228
return this._proxy.getInitialCwd(id);
226229
}

src/vs/platform/terminal/node/ptyService.ts

+58-8
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanc
1616
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
1717
import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment';
1818
import { Terminal as XtermTerminal } from 'xterm-headless';
19-
import { SerializeAddon } from 'xterm-addon-serialize';
19+
import type { SerializeAddon as XtermSerializeAddon } from 'xterm-addon-serialize';
20+
import type { Unicode11Addon as XtermUnicode11Addon } from 'xterm-addon-unicode11';
2021
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess';
2122
import { ITerminalSerializer, TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
2223
import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment';
2324
import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess';
2425

2526
type WorkspaceId = string;
2627

28+
let SerializeAddon: typeof XtermSerializeAddon;
29+
let Unicode11Addon: typeof XtermUnicode11Addon;
30+
2731
export class PtyService extends Disposable implements IPtyService {
2832
declare readonly _serviceBrand: undefined;
2933

@@ -102,6 +106,7 @@ export class PtyService extends Disposable implements IPtyService {
102106
cwd: string,
103107
cols: number,
104108
rows: number,
109+
unicodeVersion: '6' | '11',
105110
env: IProcessEnvironment,
106111
executableEnv: IProcessEnvironment,
107112
windowsEnableConpty: boolean,
@@ -125,7 +130,7 @@ export class PtyService extends Disposable implements IPtyService {
125130
if (process.onDidChangeHasChildProcesses) {
126131
process.onDidChangeHasChildProcesses(event => this._onProcessDidChangeHasChildProcesses.fire({ id, event }));
127132
}
128-
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._reconnectConstants, this._logService, shellLaunchConfig.icon);
133+
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, unicodeVersion, this._reconnectConstants, this._logService, shellLaunchConfig.icon);
129134
process.onProcessExit(() => {
130135
persistentProcess.dispose();
131136
this._ptys.delete(id);
@@ -204,6 +209,9 @@ export class PtyService extends Disposable implements IPtyService {
204209
async acknowledgeDataEvent(id: number, charCount: number): Promise<void> {
205210
return this._throwIfNoPty(id).acknowledgeDataEvent(charCount);
206211
}
212+
async setUnicodeVersion(id: number, version: '6' | '11'): Promise<void> {
213+
return this._throwIfNoPty(id).setUnicodeVersion(version);
214+
}
207215
async getLatency(id: number): Promise<number> {
208216
return 0;
209217
}
@@ -364,6 +372,7 @@ export class PersistentTerminalProcess extends Disposable {
364372
readonly shouldPersistTerminal: boolean,
365373
cols: number,
366374
rows: number,
375+
unicodeVersion: '6' | '11',
367376
reconnectConstants: IReconnectConstants,
368377
private readonly _logService: ILogService,
369378
private _icon?: TerminalIcon,
@@ -376,7 +385,8 @@ export class PersistentTerminalProcess extends Disposable {
376385
this._serializer = new XtermSerializer(
377386
cols,
378387
rows,
379-
reconnectConstants.scrollback
388+
reconnectConstants.scrollback,
389+
unicodeVersion
380390
);
381391
} else {
382392
this._serializer = new TerminalRecorder(cols, rows);
@@ -463,6 +473,10 @@ export class PersistentTerminalProcess extends Disposable {
463473
this._bufferer.flushBuffer(this._persistentProcessId);
464474
return this._terminalProcess.resize(cols, rows);
465475
}
476+
setUnicodeVersion(version: '6' | '11'): void {
477+
this._serializer.setUnicodeVersion?.(version);
478+
// TODO: Pass in unicode version in ctor
479+
}
466480
acknowledgeDataEvent(charCount: number): void {
467481
if (this._inReplay) {
468482
return;
@@ -479,8 +493,8 @@ export class PersistentTerminalProcess extends Disposable {
479493
return this._terminalProcess.getLatency();
480494
}
481495

482-
triggerReplay(): void {
483-
const ev = this._serializer.generateReplayEvent();
496+
async triggerReplay(): Promise<void> {
497+
const ev = await this._serializer.generateReplayEvent();
484498
let dataLength = 0;
485499
for (const e of ev.events) {
486500
dataLength += e.data.length;
@@ -543,8 +557,16 @@ export class PersistentTerminalProcess extends Disposable {
543557

544558
class XtermSerializer implements ITerminalSerializer {
545559
private _xterm: XtermTerminal;
546-
constructor(cols: number, rows: number, scrollback: number) {
560+
private _unicodeAddon?: XtermUnicode11Addon;
561+
562+
constructor(
563+
cols: number,
564+
rows: number,
565+
scrollback: number,
566+
unicodeVersion: '6' | '11'
567+
) {
547568
this._xterm = new XtermTerminal({ cols, rows, scrollback });
569+
this.setUnicodeVersion(unicodeVersion);
548570
}
549571

550572
handleData(data: string): void {
@@ -555,8 +577,8 @@ class XtermSerializer implements ITerminalSerializer {
555577
this._xterm.resize(cols, rows);
556578
}
557579

558-
generateReplayEvent(): IPtyHostProcessReplayEvent {
559-
const serialize = new SerializeAddon();
580+
async generateReplayEvent(): Promise<IPtyHostProcessReplayEvent> {
581+
const serialize = new (await this._getSerializeConstructor());
560582
this._xterm.loadAddon(serialize);
561583
const serialized = serialize.serialize(this._xterm.getOption('scrollback'));
562584
return {
@@ -569,6 +591,34 @@ class XtermSerializer implements ITerminalSerializer {
569591
]
570592
};
571593
}
594+
595+
async setUnicodeVersion(version: '6' | '11'): Promise<void> {
596+
if (this._xterm.unicode.activeVersion === version) {
597+
return;
598+
}
599+
if (version === '11') {
600+
this._unicodeAddon = new (await this._getUnicode11Constructor());
601+
this._xterm.loadAddon(this._unicodeAddon);
602+
} else {
603+
this._unicodeAddon?.dispose();
604+
this._unicodeAddon = undefined;
605+
}
606+
this._xterm.unicode.activeVersion = version;
607+
}
608+
609+
async _getUnicode11Constructor(): Promise<typeof Unicode11Addon> {
610+
if (!Unicode11Addon) {
611+
Unicode11Addon = (await import('xterm-addon-unicode11')).Unicode11Addon;
612+
}
613+
return Unicode11Addon;
614+
}
615+
616+
async _getSerializeConstructor(): Promise<typeof SerializeAddon> {
617+
if (!SerializeAddon) {
618+
SerializeAddon = (await import('xterm-addon-serialize')).SerializeAddon;
619+
}
620+
return SerializeAddon;
621+
}
572622
}
573623

574624
function printTime(ms: number): string {

src/vs/platform/terminal/node/terminalProcess.ts

+4
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
470470
}
471471
}
472472

473+
async setUnicodeVersion(version: '6' | '11'): Promise<void> {
474+
// No-op
475+
}
476+
473477
getInitialCwd(): Promise<string> {
474478
return Promise.resolve(this._initialCwd);
475479
}

src/vs/platform/terminal/test/common/terminalRecorder.test.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,44 @@ import * as assert from 'assert';
77
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
88
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
99

10-
function eventsEqual(recorder: TerminalRecorder, expected: ReplayEntry[]) {
11-
const actual = recorder.generateReplayEvent().events;
10+
async function eventsEqual(recorder: TerminalRecorder, expected: ReplayEntry[]) {
11+
const actual = (await recorder.generateReplayEvent()).events;
1212
for (let i = 0; i < expected.length; i++) {
1313
assert.deepStrictEqual(actual[i], expected[i]);
1414
}
1515
}
1616

1717
suite('TerminalRecorder', () => {
18-
test('should record dimensions', () => {
18+
test('should record dimensions', async () => {
1919
const recorder = new TerminalRecorder(1, 2);
20-
eventsEqual(recorder, [
20+
await eventsEqual(recorder, [
2121
{ cols: 1, rows: 2, data: '' }
2222
]);
2323
recorder.handleData('a');
2424
recorder.handleResize(3, 4);
25-
eventsEqual(recorder, [
25+
await eventsEqual(recorder, [
2626
{ cols: 1, rows: 2, data: 'a' },
2727
{ cols: 3, rows: 4, data: '' }
2828
]);
2929
});
30-
test('should ignore resize events without data', () => {
30+
test('should ignore resize events without data', async () => {
3131
const recorder = new TerminalRecorder(1, 2);
32-
eventsEqual(recorder, [
32+
await eventsEqual(recorder, [
3333
{ cols: 1, rows: 2, data: '' }
3434
]);
3535
recorder.handleResize(3, 4);
36-
eventsEqual(recorder, [
36+
await eventsEqual(recorder, [
3737
{ cols: 3, rows: 4, data: '' }
3838
]);
3939
});
40-
test('should record data and combine it into the previous resize event', () => {
40+
test('should record data and combine it into the previous resize event', async () => {
4141
const recorder = new TerminalRecorder(1, 2);
4242
recorder.handleData('a');
4343
recorder.handleData('b');
4444
recorder.handleResize(3, 4);
4545
recorder.handleData('c');
4646
recorder.handleData('d');
47-
eventsEqual(recorder, [
47+
await eventsEqual(recorder, [
4848
{ cols: 1, rows: 2, data: 'ab' },
4949
{ cols: 3, rows: 4, data: 'cd' }
5050
]);

src/vs/workbench/api/common/extHostTerminalService.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,18 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess {
263263
}
264264

265265
async processBinary(data: string): Promise<void> {
266-
// No-op, processBinary is not supported in extextion owned terminals.
266+
// No-op, processBinary is not supported in extension owned terminals.
267267
}
268268

269269
acknowledgeDataEvent(charCount: number): void {
270270
// No-op, flow control is not supported in extension owned terminals. If this is ever
271271
// implemented it will need new pause and resume VS Code APIs.
272272
}
273273

274+
async setUnicodeVersion(version: '6' | '11'): Promise<void> {
275+
// No-op, xterm-headless isn't used for extension owned terminals.
276+
}
277+
274278
getInitialCwd(): Promise<string> {
275279
return Promise.resolve('');
276280
}

src/vs/workbench/contrib/terminal/browser/remotePty.ts

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ export class RemotePty extends Disposable implements ITerminalChildProcess {
112112
});
113113
}
114114

115+
async setUnicodeVersion(version: '6' | '11'): Promise<void> {
116+
return this._remoteTerminalChannel.setUnicodeVersion(this._id, version);
117+
}
118+
115119
async getInitialCwd(): Promise<string> {
116120
await this._startBarrier.wait();
117121
return this._remoteTerminalChannel.getInitialCwd(this._id);

src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
2222
import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty';
2323
import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
2424
import { ICompleteTerminalConfiguration, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
25-
import { ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
2625
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
2726
import { IHistoryService } from 'vs/workbench/services/history/common/history';
2827
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
@@ -173,7 +172,7 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal
173172
return this._remoteTerminalChannel.acceptDetachInstanceReply(requestId, persistentProcessId);
174173
}
175174

176-
async createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise<ITerminalChildProcess> {
175+
async createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, unicodeVersion: '6' | '11', shouldPersist: boolean): Promise<ITerminalChildProcess> {
177176
if (!this._remoteTerminalChannel) {
178177
throw new Error(`Cannot create remote terminal when there is no remote!`);
179178
}
@@ -200,6 +199,7 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal
200199
shouldPersist,
201200
cols,
202201
rows,
202+
unicodeVersion
203203
);
204204
const pty = new RemotePty(result.persistentTerminalId, shouldPersist, this._remoteTerminalChannel, this._remoteAgentService, this._logService);
205205
this._ptys.set(result.persistentTerminalId, pty);

src/vs/workbench/contrib/terminal/browser/terminal.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,15 @@ export interface ITerminalFindHost {
320320
}
321321

322322
export interface IRemoteTerminalService extends IOffProcessTerminalService {
323-
createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise<ITerminalChildProcess>;
323+
createProcess(
324+
shellLaunchConfig: IShellLaunchConfig,
325+
configuration: ICompleteTerminalConfiguration,
326+
activeWorkspaceRootUri: URI | undefined,
327+
cols: number,
328+
rows: number,
329+
unicodeVersion: '6' | '11',
330+
shouldPersist: boolean
331+
): Promise<ITerminalChildProcess>;
324332
}
325333

326334
/**

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1575,7 +1575,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
15751575
this._xtermUnicode11 = new Addon();
15761576
this._xterm.loadAddon(this._xtermUnicode11);
15771577
}
1578-
this._xterm.unicode.activeVersion = this._configHelper.config.unicodeVersion;
1578+
if (this._xterm.unicode.activeVersion !== this._configHelper.config.unicodeVersion) {
1579+
this._xterm.unicode.activeVersion = this._configHelper.config.unicodeVersion;
1580+
this._processManager.setUnicodeVersion(this._configHelper.config.unicodeVersion);
1581+
}
15791582
}
15801583

15811584
updateAccessibilitySupport(): void {

src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts

+4
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal
124124
// Flow control is disabled for extension terminals
125125
}
126126

127+
async setUnicodeVersion(version: '6' | '11'): Promise<void> {
128+
// No-op
129+
}
130+
127131
async processBinary(data: string): Promise<void> {
128132
// Disabled for extension terminals
129133
this._onBinary.fire(data);

0 commit comments

Comments
 (0)