diff --git a/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts new file mode 100644 index 0000000000000000000000000000000000000000..96a5ed7300acd77b78eced31152ff3346aa67b69 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { OperatingSystem } from 'vs/base/common/platform'; +import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; + +/** + * Provides extensions to the xterm object in a modular, testable way. + */ +export class LineDataEventAddon extends DisposableStore implements ITerminalAddon { + + private _xterm?: XTermTerminal; + private _isOsSet = false; + + private readonly _onLineData = this.add(new Emitter()); + readonly onLineData = this._onLineData.event; + + activate(xterm: XTermTerminal) { + this._xterm = xterm; + // Fire onLineData when a line feed occurs, taking into account wrapped lines + xterm.onLineFeed(() => { + const buffer = xterm.buffer; + const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY); + if (newLine && !newLine.isWrapped) { + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1); + } + }); + + // Fire onLineData when disposing object to flush last line + this.add(toDisposable(() => { + const buffer = xterm.buffer; + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); + })); + } + + setOperatingSystem(os: OperatingSystem) { + if (this._isOsSet || !this._xterm) { + return; + } + this._isOsSet = true; + + // Force line data to be sent when the cursor is moved, the main purpose for + // this is because ConPTY will often not do a line feed but instead move the + // cursor, in which case we still want to send the current line's data to tasks. + if (os === OperatingSystem.Windows) { + const xterm = this._xterm; + xterm.parser.registerCsiHandler({ final: 'H' }, () => { + const buffer = xterm.buffer; + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); + return false; + }); + } + } + + private _sendLineData(buffer: IBuffer, lineIndex: number): void { + let line = buffer.getLine(lineIndex); + if (!line) { + return; + } + let lineData = line.translateToString(true); + while (lineIndex > 0 && line.isWrapped) { + line = buffer.getLine(--lineIndex); + if (!line) { + break; + } + lineData = line.translateToString(false) + lineData; + } + this._onLineData.fire(lineData); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 1e86383f37992cfa206b248f7d45f92ae9c6230b..d67d10b9ab5ce5d6ed840901124610df967a1b6b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -73,6 +73,7 @@ import { ISeparator, template } from 'vs/base/common/labels'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -645,6 +646,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); this._xterm = xterm; this._xtermCore = (xterm as any)._core as XTermCore; + const lineDataEventAddon = new LineDataEventAddon(); + this._xterm.loadAddon(lineDataEventAddon); this._updateUnicodeVersion(); this.updateAccessibilitySupport(); this._terminalInstanceService.getXtermSearchConstructor().then(addonCtor => { @@ -655,10 +658,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { this._xterm.writeln(this._shellLaunchConfig.initialText, () => { - xterm.onLineFeed(() => this._onLineFeed()); + lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); }); } else { - this._xterm.onLineFeed(() => this._onLineFeed()); + lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); } // Delay the creation of the bell listener to avoid showing the bell when the terminal // starts up or reconnects @@ -697,15 +700,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } + if (this._processManager.os) { + lineDataEventAddon.setOperatingSystem(this._processManager.os); + } if (this._processManager.os === OperatingSystem.Windows) { xterm.setOption('windowsMode', processTraits.requiresWindowsMode || false); - // Force line data to be sent when the cursor is moved, the main purpose for - // this is because ConPTY will often not do a line feed but instead move the - // cursor, in which case we still want to send the current line's data to tasks. - xterm.parser.registerCsiHandler({ final: 'H' }, () => { - this._onCursorMove(); - return false; - }); } this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!); this._areLinksReady = true; @@ -1075,11 +1074,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._horizontalScrollbar = undefined; } } - if (this._xterm) { - const buffer = this._xterm.buffer; - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); - this._xterm.dispose(); - } + this._xterm?.dispose(); if (this._pressAnyKeyToCloseListener) { this._pressAnyKeyToCloseListener.dispose(); @@ -1525,41 +1520,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.reuseTerminal(this._shellLaunchConfig, true); } - private _onLineFeed(): void { - const buffer = this._xterm!.buffer; - const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY); - if (newLine && !newLine.isWrapped) { - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1); - } - } - - private _onCursorMove(): void { - const buffer = this._xterm!.buffer; - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); - } - private _onTitleChange(title: string): void { if (this.isTitleSetByProcess) { this.refreshTabLabels(title, TitleEventSource.Sequence); } } - private _sendLineData(buffer: IBuffer, lineIndex: number): void { - let line = buffer.getLine(lineIndex); - if (!line) { - return; - } - let lineData = line.translateToString(true); - while (lineIndex > 0 && line.isWrapped) { - line = buffer.getLine(--lineIndex); - if (!line) { - break; - } - lineData = line.translateToString(false) + lineData; - } - this._onLineData.fire(lineData); - } - private _onKey(key: string, ev: KeyboardEvent): void { const event = new StandardKeyboardEvent(ev); diff --git a/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.ts b/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.ts new file mode 100644 index 0000000000000000000000000000000000000000..6feeac35f838a6278469c4929eeced26f24619db --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Terminal } from 'xterm'; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; +import { deepStrictEqual } from 'assert'; + +async function writeP(terminal: Terminal, data: string): Promise { + return new Promise(r => terminal.write(data, r)); +} + +suite.only('XtermExtensions', () => { + let xterm: Terminal; + let lineDataEventAddon: LineDataEventAddon; + + suite('onLineData', () => { + let events: string[]; + + setup(() => { + xterm = new Terminal({ + cols: 4 + }); + lineDataEventAddon = new LineDataEventAddon(); + xterm.loadAddon(lineDataEventAddon); + + events = []; + lineDataEventAddon.onLineData(e => events.push(e)); + }); + + test('should fire when a non-wrapped line ends with a \\n', async () => { + await writeP(xterm, 'foo'); + deepStrictEqual(events, []); + await writeP(xterm, '\n\r'); + deepStrictEqual(events, ['foo']); + await writeP(xterm, 'bar'); + deepStrictEqual(events, ['foo']); + await writeP(xterm, '\n'); + deepStrictEqual(events, ['foo', 'bar']); + }); + + test('should not fire soft wrapped lines', async () => { + await writeP(xterm, 'foo.'); + deepStrictEqual(events, []); + await writeP(xterm, 'bar.'); + deepStrictEqual(events, []); + await writeP(xterm, 'baz.'); + deepStrictEqual(events, []); + }); + + test('should fire when a wrapped line ends with a \\n', async () => { + await writeP(xterm, 'foo.bar.baz.'); + deepStrictEqual(events, []); + await writeP(xterm, '\n\r'); + deepStrictEqual(events, ['foo.bar.baz.']); + }); + }); +});