Skip to content

Commit 064efd6

Browse files
authored
Merge branch 'master' into patch-1
2 parents 1495e71 + f615ac5 commit 064efd6

File tree

12 files changed

+338
-171
lines changed

12 files changed

+338
-171
lines changed

addons/addon-ligatures/yarn.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ asynckit@^0.4.0:
5151
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
5252

5353
axios@^1.6.0:
54-
version "1.7.4"
55-
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2"
56-
integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==
54+
version "1.8.2"
55+
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
56+
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
5757
dependencies:
5858
follow-redirects "^1.15.6"
5959
form-data "^4.0.0"

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"chai": "^4.3.4",
8787
"cross-env": "^7.0.3",
8888
"deep-equal": "^2.0.5",
89-
"esbuild": "^0.23.0",
89+
"esbuild": "~0.25.2",
9090
"eslint": "^8.56.0",
9191
"eslint-plugin-jsdoc": "^46.9.1",
9292
"express": "^4.19.2",
@@ -95,7 +95,7 @@
9595
"jsdom": "^18.0.1",
9696
"mocha": "^10.1.0",
9797
"mustache": "^4.2.0",
98-
"node-pty": "1.1.0-beta19",
98+
"node-pty": "^1.1.0-beta31",
9999
"nyc": "^15.1.0",
100100
"source-map-loader": "^3.0.0",
101101
"source-map-support": "^0.5.20",

src/browser/CoreBrowserTerminal.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
400400
}
401401

402402
// If the terminal is already opened
403-
if (this.element?.ownerDocument.defaultView && this._coreBrowserService && this.element?.isConnected) {
403+
if (this.element?.ownerDocument.defaultView && this._coreBrowserService) {
404404
// Adjust the window if needed
405405
if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) {
406406
this._coreBrowserService.window = this.element.ownerDocument.defaultView;
@@ -532,7 +532,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
532532
this.textarea!.focus();
533533
this.textarea!.select();
534534
}));
535-
this._register(this._onScroll.event(() => this._selectionService!.refresh()));
535+
this._register(Event.any(
536+
this._onScroll.event,
537+
this._inputHandler.onScroll
538+
)(() => {
539+
this._selectionService!.refresh();
540+
this._viewport?.queueSync();
541+
}));
536542

537543
this._register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
538544
this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));

src/browser/Viewport.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ export class Viewport extends Disposable {
9393
].join('\n');
9494
}));
9595

96-
this._register(this._bufferService.onResize(() => this._queueSync()));
97-
this._register(this._bufferService.buffers.onBufferActivate(() => this._queueSync()));
96+
this._register(this._bufferService.onResize(() => this.queueSync()));
97+
this._register(this._bufferService.buffers.onBufferActivate(() => this.queueSync()));
9898
this._register(this._bufferService.onScroll(() => this._sync()));
9999

100100
this._register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
@@ -126,7 +126,7 @@ export class Viewport extends Disposable {
126126
};
127127
}
128128

129-
private _queueSync(ydisp?: number): void {
129+
public queueSync(ydisp?: number): void {
130130
// Update state
131131
if (ydisp !== undefined) {
132132
this._latestYDisp = ydisp;

src/common/InputHandler.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,37 @@ describe('InputHandler', () => {
438438
inputHandler.eraseInLine(Params.fromArray([2]));
439439
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
440440
});
441+
it('ED2 with scrollOnEraseInDisplay turned on', async () => {
442+
const inputHandler = new TestInputHandler(
443+
bufferService,
444+
new MockCharsetService(),
445+
new MockCoreService(),
446+
new MockLogService(),
447+
new MockOptionsService({ scrollOnEraseInDisplay: true }),
448+
new MockOscLinkService(),
449+
new MockCoreMouseService(),
450+
new MockUnicodeService()
451+
);
452+
const aLine = Array(bufferService.cols + 1).join('a');
453+
// add 2 full lines of text.
454+
await inputHandler.parseP(aLine);
455+
await inputHandler.parseP(aLine);
456+
457+
inputHandler.eraseInDisplay(Params.fromArray([2]));
458+
// those 2 lines should have been pushed to scrollback.
459+
assert.equal(bufferService.rows + 2, bufferService.buffer.lines.length);
460+
assert.equal(bufferService.buffer.ybase, 2);
461+
assert.equal(bufferService.buffer.lines.get(0)?.translateToString(), aLine);
462+
assert.equal(bufferService.buffer.lines.get(1)?.translateToString(), aLine);
463+
464+
// Move to last line and add more text.
465+
bufferService.buffer.y = bufferService.rows - 1;
466+
bufferService.buffer.x = 0;
467+
await inputHandler.parseP(aLine);
468+
inputHandler.eraseInDisplay(Params.fromArray([2]));
469+
// Screen should have been scrolled by a full screen size.
470+
assert.equal(bufferService.rows * 2 + 2, bufferService.buffer.lines.length);
471+
});
441472
it('eraseInDisplay', async () => {
442473
const bufferService = new MockBufferService(80, 7);
443474
const inputHandler = new TestInputHandler(

src/common/InputHandler.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,12 +1220,27 @@ export class InputHandler extends Disposable implements IInputHandler {
12201220
this._dirtyRowTracker.markDirty(0);
12211221
break;
12221222
case 2:
1223-
j = this._bufferService.rows;
1224-
this._dirtyRowTracker.markDirty(j - 1);
1225-
while (j--) {
1226-
this._resetBufferLine(j, respectProtect);
1223+
if (this._optionsService.rawOptions.scrollOnEraseInDisplay) {
1224+
j = this._bufferService.rows;
1225+
this._dirtyRowTracker.markRangeDirty(0, j - 1);
1226+
while (j--) {
1227+
const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j);
1228+
if (currentLine?.getTrimmedLength()) {
1229+
break;
1230+
}
1231+
}
1232+
for (; j >= 0; j--) {
1233+
this._bufferService.scroll(this._eraseAttrData());
1234+
}
1235+
}
1236+
else {
1237+
j = this._bufferService.rows;
1238+
this._dirtyRowTracker.markDirty(j - 1);
1239+
while (j--) {
1240+
this._resetBufferLine(j, respectProtect);
1241+
}
1242+
this._dirtyRowTracker.markDirty(0);
12271243
}
1228-
this._dirtyRowTracker.markDirty(0);
12291244
break;
12301245
case 3:
12311246
// Clear scrollback (everything not in viewport)

src/common/buffer/Buffer.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,37 @@ export class Buffer implements IBuffer {
320320
if (toRemove.length > 0) {
321321
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
322322
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
323+
324+
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
325+
// when reflowing larger is to insert empty lines at the bottom of the buffer as when lines
326+
// unwrap conpty's view cannot pull scrollback down, so it adds empty lines at the end.
327+
let removedInViewport = 0;
328+
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
329+
if (isWindowsMode) {
330+
for (let i = (toRemove.length / 2) - 1; i >= 0; i--) {
331+
if (toRemove[i * 2 + 0] > this.ybase + removedInViewport) {
332+
removedInViewport += toRemove[i * 2 + 1];
333+
}
334+
}
335+
}
336+
323337
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
338+
339+
// Apply empty lines for any removed in viewport for conpty.
340+
if (isWindowsMode) {
341+
if (removedInViewport > 0) {
342+
for (let i = 0; i < removedInViewport; i++) {
343+
// Just add the new missing rows on Windows as conpty reprints the screen with it's
344+
// view of the world. Once a line enters scrollback for conpty it remains there
345+
this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR_DATA)));
346+
}
347+
if (this.ybase === this.ydisp) {
348+
this.ydisp += removedInViewport;
349+
}
350+
this.ybase += removedInViewport;
351+
this.y -= removedInViewport;
352+
}
353+
}
324354
}
325355
}
326356

@@ -352,7 +382,7 @@ export class Buffer implements IBuffer {
352382
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
353383
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
354384
// batched up and only committed once
355-
const toInsert = [];
385+
const toInsert: { start: number, newLines: IBufferLine[] }[] = [];
356386
let countToInsert = 0;
357387
// Go backwards as many lines may be trimmed and this will avoid considering them
358388
for (let y = this.lines.length - 1; y >= 0; y--) {
@@ -467,6 +497,20 @@ export class Buffer implements IBuffer {
467497
this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);
468498
}
469499

500+
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
501+
// when reflowing smaller is to reflow all lines inside the viewport, and removing empty or
502+
// whitespace only lines from the bottom, until non-whitespace is hit in order to prevent
503+
// content from being pushed into the scrollback.
504+
let addedInViewport = 0;
505+
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
506+
if (isWindowsMode) {
507+
for (let i = toInsert.length - 1; i >= 0; i--) {
508+
if (toInsert[i].start > this.ybase + addedInViewport) {
509+
addedInViewport += toInsert[i].newLines.length;
510+
}
511+
}
512+
}
513+
470514
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
471515
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
472516
// costly calls to CircularList.splice.
@@ -520,6 +564,35 @@ export class Buffer implements IBuffer {
520564
this.lines.onTrimEmitter.fire(amountToTrim);
521565
}
522566
}
567+
568+
// Apply empty lines to remove calculated earlier for conpty.
569+
if (isWindowsMode) {
570+
if (addedInViewport > 0) {
571+
let emptyLinesAtBottom = 0;
572+
for (let i = this.lines.length - 1; i >= this.ybase + this.y; i--) {
573+
const line = this.lines.get(i) as BufferLine;
574+
if (line.isWrapped || line.getTrimmedLength() > 0) {
575+
break;
576+
}
577+
emptyLinesAtBottom++;
578+
}
579+
const emptyLinesToRemove = Math.min(addedInViewport, emptyLinesAtBottom);
580+
if (emptyLinesToRemove > 0) {
581+
for (let i = 0; i < emptyLinesToRemove; i++) {
582+
this.lines.pop();
583+
}
584+
if (this.ybase === this.ydisp) {
585+
this.ydisp -= emptyLinesToRemove;
586+
}
587+
this.ybase -= emptyLinesToRemove;
588+
this.y += emptyLinesToRemove;
589+
this.lines.onDeleteEmitter.fire({
590+
index: this.lines.length - emptyLinesToRemove,
591+
amount: emptyLinesToRemove
592+
});
593+
}
594+
}
595+
}
523596
}
524597

525598
/**

src/common/services/OptionsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
3232
logLevel: 'info',
3333
logger: null,
3434
scrollback: 1000,
35+
scrollOnEraseInDisplay: false,
3536
scrollOnUserInput: true,
3637
scrollSensitivity: 1,
3738
screenReaderMode: false,

src/common/services/Services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export interface ITerminalOptions {
254254
windowOptions?: IWindowOptions;
255255
wordSeparator?: string;
256256
overviewRuler?: IOverviewRulerOptions;
257+
scrollOnEraseInDisplay?: boolean;
257258

258259
[key: string]: any;
259260
cancelEvents: boolean;

typings/xterm-headless.d.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ declare module '@xterm/headless' {
186186
*/
187187
scrollback?: number;
188188

189+
/**
190+
* If enabled the Erase in Display All (ED2) escape sequence will push
191+
* erased text to scrollback, instead of clearing only the viewport portion.
192+
* This emulates PuTTY's default clear screen behavior.
193+
*/
194+
scrollOnEraseInDisplay?: boolean;
195+
189196
/**
190197
* The scrolling speed multiplier used for adjusting normal scrolling speed.
191198
*/
@@ -826,21 +833,31 @@ declare module '@xterm/headless' {
826833

827834
/**
828835
* Write data to the terminal.
836+
*
837+
* Note that the change will not be reflected in the {@link buffer}
838+
* immediately as the data is processed asynchronously. Provide a
839+
* {@link callback} to know when the data was processed.
829840
* @param data The data to write to the terminal. This can either be raw
830841
* bytes given as Uint8Array from the pty or a string. Raw bytes will always
831842
* be treated as UTF-8 encoded, string data as UTF-16.
832843
* @param callback Optional callback that fires when the data was processed
833-
* by the parser.
844+
* by the parser. This callback must be provided and awaited in order for
845+
* {@link buffer} to reflect the change in the write.
834846
*/
835847
write(data: string | Uint8Array, callback?: () => void): void;
836848

837849
/**
838850
* Writes data to the terminal, followed by a break line character (\n).
851+
*
852+
* Note that the change will not be reflected in the {@link buffer}
853+
* immediately as the data is processed asynchronously. Provide a
854+
* {@link callback} to know when the data was processed.
839855
* @param data The data to write to the terminal. This can either be raw
840856
* bytes given as Uint8Array from the pty or a string. Raw bytes will always
841857
* be treated as UTF-8 encoded, string data as UTF-16.
842858
* @param callback Optional callback that fires when the data was processed
843-
* by the parser.
859+
* by the parser. This callback must be provided and awaited in order for
860+
* {@link buffer} to reflect the change in the write.
844861
*/
845862
writeln(data: string | Uint8Array, callback?: () => void): void;
846863

0 commit comments

Comments
 (0)