Skip to content

Commit fb2c39c

Browse files
authored
Merge pull request #4877 from Tyriar/serialize_fixes
Add range API to serialize addon
2 parents 49eab6b + b595940 commit fb2c39c

File tree

3 files changed

+102
-58
lines changed

3 files changed

+102
-58
lines changed

addons/addon-serialize/src/SerializeAddon.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { SerializeAddon } from './SerializeAddon';
99
import { Terminal } from 'browser/public/Terminal';
1010
import { SelectionModel } from 'browser/selection/SelectionModel';
1111
import { IBufferService } from 'common/services/Services';
12-
import { OptionsService } from 'common/services/OptionsService';
1312
import { ThemeService } from 'browser/services/ThemeService';
1413

1514
function sgr(...seq: string[]): string {
@@ -83,6 +82,36 @@ describe('SerializeAddon', () => {
8382
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
8483
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
8584
});
85+
86+
describe('ISerializeOptions.range', () => {
87+
it('should serialize the top line', async () => {
88+
await writeP(terminal, 'hello\r\nworld');
89+
assert.equal(serializeAddon.serialize({
90+
range: {
91+
start: 0,
92+
end: 0
93+
}
94+
}), 'hello');
95+
});
96+
it('should serialize multiple lines from the top', async () => {
97+
await writeP(terminal, 'hello\r\nworld');
98+
assert.equal(serializeAddon.serialize({
99+
range: {
100+
start: 0,
101+
end: 1
102+
}
103+
}), 'hello\r\nworld');
104+
});
105+
it('should serialize lines in the middle', async () => {
106+
await writeP(terminal, 'hello\r\nworld');
107+
assert.equal(serializeAddon.serialize({
108+
range: {
109+
start: 1,
110+
end: 1
111+
}
112+
}), 'world');
113+
});
114+
});
86115
});
87116

88117
describe('html', () => {

addons/addon-serialize/src/SerializeAddon.ts

Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
9-
import type { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize';
9+
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
1010
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
1111
import { IAttributeData, IColor } from 'common/Types';
1212

@@ -21,24 +21,24 @@ abstract class BaseSerializeHandler {
2121
) {
2222
}
2323

24-
public serialize(range: IBufferRange): string {
24+
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
2525
// we need two of them to flip between old and new cell
2626
const cell1 = this._buffer.getNullCell();
2727
const cell2 = this._buffer.getNullCell();
2828
let oldCell = cell1;
2929

30-
const startRow = range.start.x;
31-
const endRow = range.end.x;
32-
const startColumn = range.start.y;
33-
const endColumn = range.end.y;
30+
const startRow = range.start.y;
31+
const endRow = range.end.y;
32+
const startColumn = range.start.x;
33+
const endColumn = range.end.x;
3434

3535
this._beforeSerialize(endRow - startRow, startRow, endRow);
3636

3737
for (let row = startRow; row <= endRow; row++) {
3838
const line = this._buffer.getLine(row);
3939
if (line) {
40-
const startLineColumn = row !== range.start.x ? 0 : startColumn;
41-
const endLineColumn = row !== range.end.x ? line.length : endColumn;
40+
const startLineColumn = row === range.start.y ? startColumn : 0;
41+
const endLineColumn = row === range.end.y ? endColumn: line.length;
4242
for (let col = startLineColumn; col < endLineColumn; col++) {
4343
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
4444
if (!c) {
@@ -54,14 +54,14 @@ abstract class BaseSerializeHandler {
5454

5555
this._afterSerialize();
5656

57-
return this._serializeString();
57+
return this._serializeString(excludeFinalCursorPosition);
5858
}
5959

6060
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
6161
protected _rowEnd(row: number, isLastRow: boolean): void { }
6262
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
6363
protected _afterSerialize(): void { }
64-
protected _serializeString(): string { return ''; }
64+
protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }
6565
}
6666

6767
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
@@ -353,7 +353,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
353353
}
354354
}
355355

356-
protected _serializeString(): string {
356+
protected _serializeString(excludeFinalCursorPosition: boolean): string {
357357
let rowEnd = this._allRows.length;
358358

359359
// the fixup is only required for data without scrollback
@@ -374,29 +374,31 @@ class StringSerializeHandler extends BaseSerializeHandler {
374374
}
375375

376376
// restore the cursor
377-
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
378-
const realCursorCol = this._buffer.cursorX;
377+
if (!excludeFinalCursorPosition) {
378+
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
379+
const realCursorCol = this._buffer.cursorX;
379380

380-
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
381+
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
381382

382-
const moveRight = (offset: number): void => {
383-
if (offset > 0) {
384-
content += `\u001b[${offset}C`;
385-
} else if (offset < 0) {
386-
content += `\u001b[${-offset}D`;
387-
}
388-
};
389-
const moveDown = (offset: number): void => {
390-
if (offset > 0) {
391-
content += `\u001b[${offset}B`;
392-
} else if (offset < 0) {
393-
content += `\u001b[${-offset}A`;
394-
}
395-
};
383+
const moveRight = (offset: number): void => {
384+
if (offset > 0) {
385+
content += `\u001b[${offset}C`;
386+
} else if (offset < 0) {
387+
content += `\u001b[${-offset}D`;
388+
}
389+
};
390+
const moveDown = (offset: number): void => {
391+
if (offset > 0) {
392+
content += `\u001b[${offset}B`;
393+
} else if (offset < 0) {
394+
content += `\u001b[${-offset}A`;
395+
}
396+
};
396397

397-
if (cursorMoved) {
398-
moveDown(realCursorRow - this._lastCursorRow);
399-
moveRight(realCursorCol - this._lastCursorCol);
398+
if (cursorMoved) {
399+
moveDown(realCursorRow - this._lastCursorRow);
400+
moveRight(realCursorCol - this._lastCursorCol);
401+
}
400402
}
401403

402404
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
@@ -419,14 +421,21 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
419421
this._terminal = terminal;
420422
}
421423

422-
private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
424+
private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
423425
const maxRows = buffer.length;
424-
const handler = new StringSerializeHandler(buffer, terminal);
425426
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
427+
return this._serializeBufferByRange(terminal, buffer, {
428+
start: maxRows - correctRows,
429+
end: maxRows - 1
430+
}, false);
431+
}
432+
433+
private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string {
434+
const handler = new StringSerializeHandler(buffer, terminal);
426435
return handler.serialize({
427-
start: { x: maxRows - correctRows, y: 0 },
428-
end: { x: maxRows - 1, y: terminal.cols }
429-
});
436+
start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line },
437+
end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line }
438+
}, excludeFinalCursorPosition);
430439
}
431440

432441
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
@@ -438,16 +447,16 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
438447
const scrollback = options.scrollback;
439448
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
440449
return handler.serialize({
441-
start: { x: maxRows - correctRows, y: 0 },
442-
end: { x: maxRows - 1, y: terminal.cols }
450+
start: { x: 0, y: maxRows - correctRows },
451+
end: { x: terminal.cols, y: maxRows - 1 }
443452
});
444453
}
445454

446455
const selection = this._terminal?.getSelectionPosition();
447456
if (selection !== undefined) {
448457
return handler.serialize({
449-
start: { x: selection.start.y, y: selection.start.x },
450-
end: { x: selection.end.y, y: selection.end.x }
458+
start: { x: selection.start.x, y: selection.start.y },
459+
end: { x: selection.end.x, y: selection.end.y }
451460
});
452461
}
453462

@@ -490,12 +499,14 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
490499
}
491500

492501
// Normal buffer
493-
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
502+
let content = options?.range
503+
? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true)
504+
: this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback);
494505

495506
// Alternate buffer
496507
if (!options?.excludeAltBuffer) {
497508
if (this._terminal.buffer.active.type === 'alternate') {
498-
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
509+
const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined);
499510
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
500511
}
501512
}
@@ -519,19 +530,6 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
519530
public dispose(): void { }
520531
}
521532

522-
523-
interface ISerializeOptions {
524-
scrollback?: number;
525-
excludeModes?: boolean;
526-
excludeAltBuffer?: boolean;
527-
}
528-
529-
interface IHTMLSerializeOptions {
530-
scrollback: number;
531-
onlySelection: boolean;
532-
includeGlobalBackground: boolean;
533-
}
534-
535533
export class HTMLSerializeHandler extends BaseSerializeHandler {
536534
private _currentRow: string = '';
537535

addons/addon-serialize/typings/addon-serialize.d.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @license MIT
44
*/
55

6-
import { Terminal, ITerminalAddon } from '@xterm/xterm';
6+
import { Terminal, ITerminalAddon, IMarker, IBufferRange } from '@xterm/xterm';
77

88
declare module '@xterm/addon-serialize' {
99
/**
@@ -48,10 +48,16 @@ declare module '@xterm/addon-serialize' {
4848
}
4949

5050
export interface ISerializeOptions {
51+
/**
52+
* The row range to serialize. The an explicit range is specified, the cursor will get its final
53+
* repositioning.
54+
*/
55+
range?: ISerializeRange;
56+
5157
/**
5258
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
5359
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
54-
* serialized.
60+
* serialized. This will be ignored if {@link range} is specified.
5561
*/
5662
scrollback?: number;
5763

@@ -85,4 +91,15 @@ declare module '@xterm/addon-serialize' {
8591
*/
8692
includeGlobalBackground: boolean;
8793
}
94+
95+
export interface ISerializeRange {
96+
/**
97+
* The line to start serializing (inclusive).
98+
*/
99+
start: IMarker | number;
100+
/**
101+
* The line to end serializing (inclusive).
102+
*/
103+
end: IMarker | number;
104+
}
88105
}

0 commit comments

Comments
 (0)