From b80832ac4cd6a92b2dc3d031b57ab891ea055eab Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Mar 2018 12:52:38 -0700 Subject: [PATCH 1/6] Initial marker API implementation Part of #1325 --- src/Buffer.ts | 49 +++++++++++++++++++++++++++++++++++++ src/SelectionManager.ts | 10 ++++++++ src/Terminal.ts | 29 +++++++++++++++++++++- src/utils/TestUtils.test.ts | 12 ++++++++- typings/xterm.d.ts | 32 ++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7e34a23d..e4c4468e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -5,6 +5,8 @@ import { CircularList } from './utils/CircularList'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; +import { EventEmitter } from './EventEmitter'; +import { IDisposable, IMarker } from 'xterm'; export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; @@ -31,6 +33,7 @@ export class Buffer implements IBuffer { public tabs: any; public savedY: number; public savedX: number; + public markers: Marker[] = []; /** * Create a new Buffer. @@ -303,4 +306,50 @@ export class Buffer implements IBuffer { while (!this.tabs[++x] && x < this._terminal.cols); return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; } + + public addMarker(y: number): Marker { + const marker = new Marker(y); + this.markers.push(marker); + marker.disposables.push(this._lines.addDisposableListener('trim', amount => { + marker.line -= amount; + // The marker should be disposed when the line is trimmed from the buffer + if (marker.line < 0) { + marker.dispose(); + } + // TODO: handle splice? + })); + marker.on('dispose', () => this._removeMarker(marker)); + return marker; + } + + private _removeMarker(marker: Marker): void { + // TODO: This could probably be optimized by relying on sort order and trimming the array using .length + this.markers = this.markers.splice(this.markers.indexOf(marker), 1); + } +} + +export class Marker extends EventEmitter implements IMarker { + private static NEXT_ID = 1; + + private _id: number = Marker.NEXT_ID++; + public isDisposed: boolean = false; + public disposables: IDisposable[] = []; + + public get id(): number { return this._id; } + + constructor( + public line: number + ) { + super(); + } + + public dispose(): void { + if (this.isDisposed) { + return; + } + this.isDisposed = true; + this.disposables.forEach(d => d.dispose()); + this.disposables.length = 0; + this.emit('dispose'); + } } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 506e87d4..a50203bd 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -292,6 +292,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._terminal.emit('selection'); } + public selectLines(start: number, end: number): void { + this._model.clearSelection(); + start = Math.max(start, 0); + end = Math.min(end, this._terminal.buffer.lines.length - 1); + this._model.selectionStart = [0, start]; + this._model.selectionEnd = [this._terminal.cols, end]; + this.refresh(); + this._terminal.emit('selection'); + } + /** * Handle the buffer being trimmed, adjust the selection position. * @param amount The amount the buffer is being trimmed. diff --git a/src/Terminal.ts b/src/Terminal.ts index 924260ca..22aafdb4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -45,7 +45,7 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './input/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; -import { ITheme, ILocalizableStrings } from 'xterm'; +import { ITheme, ILocalizableStrings, IMarker } from 'xterm'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -1241,6 +1241,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.scrollLines(this.buffer.ybase - this.buffer.ydisp); } + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + console.log('scrollAmount', scrollAmount); + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } + /** * Writes text to the terminal. * @param {string} data The text to write to the terminal. @@ -1349,6 +1357,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } + public get markers(): IMarker[] { + return this.buffer.markers; + } + + public addMarker(cursorYOffset: number): IMarker { + // Disallow markers on the alt buffer + if (this.buffer !== this.buffers.normal) { + return; + } + + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); + } + /** * Gets whether the terminal has an active selection. */ @@ -1382,6 +1403,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } + public selectLines(start: number, end: number): void { + if (this.selectionManager) { + this.selectionManager.selectLines(start, end); + } + } + /** * Handle a keydown event * Key Resources: diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 3c60d695..5f3a8482 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -7,9 +7,19 @@ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../rende import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; -import { ITheme, IDisposable } from 'xterm'; +import { ITheme, IDisposable, IMarker } from 'xterm'; export class MockTerminal implements ITerminal { + markers: IMarker[]; + addMarker(cursorYOffset: number): IMarker { + throw new Error('Method not implemented.'); + } + selectLines(start: number, end: number): void { + throw new Error('Method not implemented.'); + } + scrollToLine(line: number): void { + throw new Error('Method not implemented.'); + } static string: any; getOption(key: any): any { throw new Error('Method not implemented.'); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0e8fdcc7..0d6e422d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -235,6 +235,12 @@ declare module 'xterm' { dispose(): void; } + export interface IMarker extends IDisposable { + readonly id: number; + readonly isDisposed: boolean; + readonly line: number; + } + export interface ILocalizableStrings { blankLine: string; promptLabel: string; @@ -265,6 +271,12 @@ declare module 'xterm' { */ cols: number; + /** + * Get all markers registered against the buffer. If the alt buffer is + * active this will always return []. + */ + markers: IMarker[]; + /** * Natural language strings that can be localized. */ @@ -403,6 +415,13 @@ declare module 'xterm' { */ deregisterLinkMatcher(matcherId: number): void; + /** + * Adds a marker to the normal buffer and returns it. If the alt buffer is + * active, undefined is returned. + * @param cursorYOffset The y position offset of the marker from the cursor. + */ + addMarker(cursorYOffset: number): IMarker; + /** * Gets whether the terminal has an active selection. */ @@ -424,6 +443,13 @@ declare module 'xterm' { */ selectAll(): void; + /** + * Selects text in the buffer between 2 lines. + * @param start The 0-based line index to select from (inclusive). + * @param end The 0-based line index to select to (inclusive). + */ + selectLines(start: number, end: number): void; + /** * Destroys the terminal and detaches it from the DOM. */ @@ -451,6 +477,12 @@ declare module 'xterm' { */ scrollToBottom(): void; + /** + * Scrolls to a line within the buffer. + * @param line The 0-based line index to scroll to. + */ + scrollToLine(line: number): void; + /** * Clear the entire buffer, making the prompt line the new first line. */ From cbc6d7ce3243657537951fbb4b16987477657b4e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Mar 2018 15:56:48 -0700 Subject: [PATCH 2/6] Add selectLines test --- src/SelectionManager.test.ts | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 3dae2c31..8d0b04aa 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -314,6 +314,45 @@ describe('SelectionManager', () => { }); }); + describe('selectLines', () => { + it('should select a single line', () => { + buffer.lines.length = 3; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + selectionManager.selectLines(1, 1); + assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + }); + it('should select multiple lines', () => { + buffer.lines.length = 5; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + buffer.lines.set(3, stringToRow('4')); + buffer.lines.set(4, stringToRow('5')); + selectionManager.selectLines(1, 3); + assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]); + }); + it('should select the to the start when requesting a negative row', () => { + buffer.lines.length = 2; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + selectionManager.selectLines(-1, 0); + assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]); + }); + it('should select the to the end when requesting beyond the final row', () => { + buffer.lines.length = 2; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + selectionManager.selectLines(1, 2); + assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + }); + }); + describe('hasSelection', () => { it('should return whether there is a selection', () => { selectionManager.model.selectionStart = [0, 0]; From 5c1a4a7d01c93e6194397c8a8b1125c6932b9873 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Mar 2018 16:34:14 -0700 Subject: [PATCH 3/6] Add tests for Buffer.addMarker --- src/Buffer.test.ts | 24 ++++++++++++++++++++++++ src/Buffer.ts | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0607a573..44687f0e 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -188,4 +188,28 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS / 2); }); }); + + describe('addMarker', () => { + it('should adjust a marker line when the buffer is trimmed', () => { + terminal.options.scrollback = 0; + buffer = new Buffer(terminal, true); + buffer.fillViewportRows(); + const marker = buffer.addMarker(buffer.lines.length - 1); + assert.equal(marker.line, buffer.lines.length - 1); + buffer.lines.emit('trim', 1); + assert.equal(marker.line, buffer.lines.length - 2); + }); + it('should dispose of a marker if it is trimmed off the buffer', () => { + terminal.options.scrollback = 0; + buffer = new Buffer(terminal, true); + buffer.fillViewportRows(); + assert.equal(buffer.markers.length, 0); + const marker = buffer.addMarker(0); + assert.equal(marker.isDisposed, false); + assert.equal(buffer.markers.length, 1); + buffer.lines.emit('trim', 1); + assert.equal(marker.isDisposed, true); + assert.equal(buffer.markers.length, 0); + }); + }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index e4c4468e..ddefa8ed 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -311,6 +311,7 @@ export class Buffer implements IBuffer { const marker = new Marker(y); this.markers.push(marker); marker.disposables.push(this._lines.addDisposableListener('trim', amount => { + console.log('trim!' + amount); marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { @@ -324,7 +325,7 @@ export class Buffer implements IBuffer { private _removeMarker(marker: Marker): void { // TODO: This could probably be optimized by relying on sort order and trimming the array using .length - this.markers = this.markers.splice(this.markers.indexOf(marker), 1); + this.markers.splice(this.markers.indexOf(marker), 1); } } From cbf74196018fe7449c3cc4222a0aecea4144e344 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Mar 2018 17:24:04 -0700 Subject: [PATCH 4/6] Flag markers and addMarker as experimental --- typings/xterm.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0d6e422d..ea22b076 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -272,8 +272,8 @@ declare module 'xterm' { cols: number; /** - * Get all markers registered against the buffer. If the alt buffer is - * active this will always return []. + * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt + * buffer is active this will always return []. */ markers: IMarker[]; @@ -416,8 +416,8 @@ declare module 'xterm' { deregisterLinkMatcher(matcherId: number): void; /** - * Adds a marker to the normal buffer and returns it. If the alt buffer is - * active, undefined is returned. + * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the + * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. */ addMarker(cursorYOffset: number): IMarker; From 387d5b056d927a87827d282ee65f6c8ca2504608 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 17 Mar 2018 11:46:37 -0700 Subject: [PATCH 5/6] Add Terminal.scrollToLine tests --- src/Terminal.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 0db9545f..d47fbdaf 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -258,6 +258,34 @@ describe('term.js addons', () => { }); }); + describe('scrollToLine', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll to requested line', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(0); + assert.equal(term.buffer.ydisp, 0); + term.scrollToLine(10); + assert.equal(term.buffer.ydisp, 10); + term.scrollToLine(startYDisp); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(20); + assert.equal(term.buffer.ydisp, 20); + }); + it('should not scroll beyond boundary lines', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(-1); + assert.equal(term.buffer.ydisp, 0); + term.scrollToLine(startYDisp + 1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + describe('keyDown', () => { it('should scroll down, when a key is pressed and terminal is scrolled up', () => { // Override _evaluateKeyEscapeSequence to return cancel code From 0eeec13653928607a9870334ee8a755e9f92daa5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 19 Mar 2018 11:01:26 -0700 Subject: [PATCH 6/6] Clean up --- src/Buffer.ts | 2 -- src/Terminal.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ddefa8ed..c1cf3cc7 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -311,13 +311,11 @@ export class Buffer implements IBuffer { const marker = new Marker(y); this.markers.push(marker); marker.disposables.push(this._lines.addDisposableListener('trim', amount => { - console.log('trim!' + amount); marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { marker.dispose(); } - // TODO: handle splice? })); marker.on('dispose', () => this._removeMarker(marker)); return marker; diff --git a/src/Terminal.ts b/src/Terminal.ts index 22aafdb4..b818de3d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1243,7 +1243,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public scrollToLine(line: number): void { const scrollAmount = line - this.buffer.ydisp; - console.log('scrollAmount', scrollAmount); if (scrollAmount !== 0) { this.scrollLines(scrollAmount); }