Skip to content

Commit 01fa1f6

Browse files
committed
Move onLineData to an xterm addon and test
Part of microsoft#133757
1 parent 4d69434 commit 01fa1f6

File tree

3 files changed

+142
-43
lines changed

3 files changed

+142
-43
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Emitter } from 'vs/base/common/event';
7+
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
8+
import { OperatingSystem } from 'vs/base/common/platform';
9+
import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm';
10+
11+
/**
12+
* Provides extensions to the xterm object in a modular, testable way.
13+
*/
14+
export class LineDataEventAddon extends DisposableStore implements ITerminalAddon {
15+
16+
private _xterm?: XTermTerminal;
17+
private _isOsSet = false;
18+
19+
private readonly _onLineData = this.add(new Emitter<string>());
20+
readonly onLineData = this._onLineData.event;
21+
22+
activate(xterm: XTermTerminal) {
23+
this._xterm = xterm;
24+
// Fire onLineData when a line feed occurs, taking into account wrapped lines
25+
xterm.onLineFeed(() => {
26+
const buffer = xterm.buffer;
27+
const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY);
28+
if (newLine && !newLine.isWrapped) {
29+
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1);
30+
}
31+
});
32+
33+
// Fire onLineData when disposing object to flush last line
34+
this.add(toDisposable(() => {
35+
const buffer = xterm.buffer;
36+
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
37+
}));
38+
}
39+
40+
setOperatingSystem(os: OperatingSystem) {
41+
if (this._isOsSet || !this._xterm) {
42+
return;
43+
}
44+
this._isOsSet = true;
45+
46+
// Force line data to be sent when the cursor is moved, the main purpose for
47+
// this is because ConPTY will often not do a line feed but instead move the
48+
// cursor, in which case we still want to send the current line's data to tasks.
49+
if (os === OperatingSystem.Windows) {
50+
const xterm = this._xterm;
51+
xterm.parser.registerCsiHandler({ final: 'H' }, () => {
52+
const buffer = xterm.buffer;
53+
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
54+
return false;
55+
});
56+
}
57+
}
58+
59+
private _sendLineData(buffer: IBuffer, lineIndex: number): void {
60+
let line = buffer.getLine(lineIndex);
61+
if (!line) {
62+
return;
63+
}
64+
let lineData = line.translateToString(true);
65+
while (lineIndex > 0 && line.isWrapped) {
66+
line = buffer.getLine(--lineIndex);
67+
if (!line) {
68+
break;
69+
}
70+
lineData = line.translateToString(false) + lineData;
71+
}
72+
this._onLineData.fire(lineData);
73+
}
74+
}

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

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { ISeparator, template } from 'vs/base/common/labels';
7373
import { IPathService } from 'vs/workbench/services/path/common/pathService';
7474
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
7575
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
76+
import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon';
7677

7778
// How long in milliseconds should an average frame take to render for a notification to appear
7879
// which suggests the fallback DOM-based renderer
@@ -645,6 +646,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
645646
});
646647
this._xterm = xterm;
647648
this._xtermCore = (xterm as any)._core as XTermCore;
649+
const lineDataEventAddon = new LineDataEventAddon();
650+
this._xterm.loadAddon(lineDataEventAddon);
648651
this._updateUnicodeVersion();
649652
this.updateAccessibilitySupport();
650653
this._terminalInstanceService.getXtermSearchConstructor().then(addonCtor => {
@@ -655,10 +658,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
655658
// onLineData events containing initialText
656659
if (this._shellLaunchConfig.initialText) {
657660
this._xterm.writeln(this._shellLaunchConfig.initialText, () => {
658-
xterm.onLineFeed(() => this._onLineFeed());
661+
lineDataEventAddon.onLineData(e => this._onLineData.fire(e));
659662
});
660663
} else {
661-
this._xterm.onLineFeed(() => this._onLineFeed());
664+
lineDataEventAddon.onLineData(e => this._onLineData.fire(e));
662665
}
663666
// Delay the creation of the bell listener to avoid showing the bell when the terminal
664667
// starts up or reconnects
@@ -697,15 +700,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
697700
return;
698701
}
699702

703+
if (this._processManager.os) {
704+
lineDataEventAddon.setOperatingSystem(this._processManager.os);
705+
}
700706
if (this._processManager.os === OperatingSystem.Windows) {
701707
xterm.setOption('windowsMode', processTraits.requiresWindowsMode || false);
702-
// Force line data to be sent when the cursor is moved, the main purpose for
703-
// this is because ConPTY will often not do a line feed but instead move the
704-
// cursor, in which case we still want to send the current line's data to tasks.
705-
xterm.parser.registerCsiHandler({ final: 'H' }, () => {
706-
this._onCursorMove();
707-
return false;
708-
});
709708
}
710709
this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!);
711710
this._areLinksReady = true;
@@ -1075,11 +1074,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
10751074
this._horizontalScrollbar = undefined;
10761075
}
10771076
}
1078-
if (this._xterm) {
1079-
const buffer = this._xterm.buffer;
1080-
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
1081-
this._xterm.dispose();
1082-
}
1077+
this._xterm?.dispose();
10831078

10841079
if (this._pressAnyKeyToCloseListener) {
10851080
this._pressAnyKeyToCloseListener.dispose();
@@ -1525,41 +1520,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
15251520
this.reuseTerminal(this._shellLaunchConfig, true);
15261521
}
15271522

1528-
private _onLineFeed(): void {
1529-
const buffer = this._xterm!.buffer;
1530-
const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY);
1531-
if (newLine && !newLine.isWrapped) {
1532-
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1);
1533-
}
1534-
}
1535-
1536-
private _onCursorMove(): void {
1537-
const buffer = this._xterm!.buffer;
1538-
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
1539-
}
1540-
15411523
private _onTitleChange(title: string): void {
15421524
if (this.isTitleSetByProcess) {
15431525
this.refreshTabLabels(title, TitleEventSource.Sequence);
15441526
}
15451527
}
15461528

1547-
private _sendLineData(buffer: IBuffer, lineIndex: number): void {
1548-
let line = buffer.getLine(lineIndex);
1549-
if (!line) {
1550-
return;
1551-
}
1552-
let lineData = line.translateToString(true);
1553-
while (lineIndex > 0 && line.isWrapped) {
1554-
line = buffer.getLine(--lineIndex);
1555-
if (!line) {
1556-
break;
1557-
}
1558-
lineData = line.translateToString(false) + lineData;
1559-
}
1560-
this._onLineData.fire(lineData);
1561-
}
1562-
15631529
private _onKey(key: string, ev: KeyboardEvent): void {
15641530
const event = new StandardKeyboardEvent(ev);
15651531

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Terminal } from 'xterm';
7+
import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon';
8+
import { deepStrictEqual } from 'assert';
9+
10+
async function writeP(terminal: Terminal, data: string): Promise<void> {
11+
return new Promise<void>(r => terminal.write(data, r));
12+
}
13+
14+
suite.only('XtermExtensions', () => {
15+
let xterm: Terminal;
16+
let lineDataEventAddon: LineDataEventAddon;
17+
18+
suite('onLineData', () => {
19+
let events: string[];
20+
21+
setup(() => {
22+
xterm = new Terminal({
23+
cols: 4
24+
});
25+
lineDataEventAddon = new LineDataEventAddon();
26+
xterm.loadAddon(lineDataEventAddon);
27+
28+
events = [];
29+
lineDataEventAddon.onLineData(e => events.push(e));
30+
});
31+
32+
test('should fire when a non-wrapped line ends with a \\n', async () => {
33+
await writeP(xterm, 'foo');
34+
deepStrictEqual(events, []);
35+
await writeP(xterm, '\n\r');
36+
deepStrictEqual(events, ['foo']);
37+
await writeP(xterm, 'bar');
38+
deepStrictEqual(events, ['foo']);
39+
await writeP(xterm, '\n');
40+
deepStrictEqual(events, ['foo', 'bar']);
41+
});
42+
43+
test('should not fire soft wrapped lines', async () => {
44+
await writeP(xterm, 'foo.');
45+
deepStrictEqual(events, []);
46+
await writeP(xterm, 'bar.');
47+
deepStrictEqual(events, []);
48+
await writeP(xterm, 'baz.');
49+
deepStrictEqual(events, []);
50+
});
51+
52+
test('should fire when a wrapped line ends with a \\n', async () => {
53+
await writeP(xterm, 'foo.bar.baz.');
54+
deepStrictEqual(events, []);
55+
await writeP(xterm, '\n\r');
56+
deepStrictEqual(events, ['foo.bar.baz.']);
57+
});
58+
});
59+
});

0 commit comments

Comments
 (0)