Merge remote-tracking branch 'origin/master' into 998_mouse_off

This commit is contained in:
Daniel Imms
2018-07-02 13:22:28 -07:00
42 changed files with 644 additions and 241 deletions
+1
View File
@@ -32,6 +32,7 @@
</div>
<hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+44 -22
View File
@@ -33,23 +33,25 @@ function setPadding() {
term.fit();
}
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
createTerminal();
const disposeRecreateButtonHandler = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
term = null;
window.term = null;
socket = null;
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
}
else {
createTerminal();
document.getElementById('dispose').innerHTML = 'Dispose terminal';
}
};
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
@@ -76,6 +78,21 @@ function createTerminal() {
term.fit();
term.focus();
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
initOptions(term);
@@ -124,7 +141,7 @@ function runFakeTerminal() {
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
@@ -139,11 +156,11 @@ function runFakeTerminal() {
} else if (printable) {
term.write(key);
}
});
}));
term.on('paste', function (data, ev) {
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term.write(data);
});
}));
}
function initOptions(term) {
@@ -213,14 +230,14 @@ function initOptions(term) {
// Attach listeners
booleanOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
@@ -231,13 +248,18 @@ function initOptions(term) {
});
Object.keys(stringOptions).forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
});
});
}
function addDomListener(element, type, handler) {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
+1 -1
View File
@@ -30,7 +30,7 @@
"jsdoc": "3.4.3",
"jsdom": "^11.11.0",
"merge-stream": "^1.0.1",
"node-pty": "^0.7.2",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
+18 -20
View File
@@ -6,9 +6,9 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
BOTTOM
}
export class AccessibilityManager implements IDisposable {
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
+78 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
@@ -269,4 +269,81 @@ describe('Buffer', () => {
assert.equal(buffer.markers.length, 0);
});
});
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
buffer.lines.set(0, [
[ null, 'a', 1, 'a'.charCodeAt(0)],
[ null, 'b', 1, 'b'.charCodeAt(0)],
[ null, 'c', 1, 'c'.charCodeAt(0)],
[ null, 'd', 1, 'd'.charCodeAt(0)]
]);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '語');
});
it('should handle a zero width character in the middle of the string by not including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str1, '語');
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str2, '語a');
});
it('should handle single width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁a');
});
it('should handle double width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str3, '😁a');
});
});
});
+7 -8
View File
@@ -3,10 +3,10 @@
* @license MIT
*/
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
import { IMarker } from 'xterm';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
@@ -227,7 +227,7 @@ export class Buffer implements IBuffer {
if (startCol >= i) {
startIndex--;
}
if (endCol >= i) {
if (endCol > i) {
endIndex--;
}
} else {
@@ -320,14 +320,14 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.register(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();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
@@ -342,7 +342,6 @@ export class Marker extends EventEmitter implements IMarker {
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
@@ -357,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+22 -1
View File
@@ -4,6 +4,7 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { Disposable } from './common/Lifecycle';
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
@@ -207,7 +208,7 @@ class DcsDummy implements IDcsHandler {
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser implements IEscapeSequenceParser {
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
@@ -236,6 +237,8 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
@@ -260,6 +263,24 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
this._errorHandler = this._errorHandlerFb;
}
public dispose(): void {
this._printHandlerFb = null;
this._executeHandlerFb = null;
this._csiHandlerFb = null;
this._escHandlerFb = null;
this._oscHandlerFb = null;
this._dcsHandlerFb = null;
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
this._errorHandler = null;
}
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback;
}
+5 -1
View File
@@ -5,11 +5,13 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter, IDisposable {
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
@@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
return {
dispose: () => {
@@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
}
public dispose(): void {
super.dispose();
this._events = {};
}
}
+4
View File
@@ -12,18 +12,22 @@ describe('InputHandler', () => {
const terminal = new MockInputHandlingTerminal();
terminal.buffer.x = 1;
terminal.buffer.y = 2;
terminal.curAttr = 3;
const inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
terminal.curAttr = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
+13 -1
View File
@@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -111,13 +112,17 @@ class DECRQSS implements IDcsHandler {
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler implements IInputHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
this.register(this._parser);
this._surrogateHigh = '';
/**
@@ -285,6 +290,11 @@ export class InputHandler implements IInputHandler {
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
super.dispose();
this._terminal = null;
}
public parse(data: string): void {
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
@@ -1823,6 +1833,7 @@ export class InputHandler implements IInputHandler {
public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.savedCurAttr = this._terminal.curAttr;
}
@@ -1834,6 +1845,7 @@ export class InputHandler implements IInputHandler {
public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR;
}
+4 -2
View File
@@ -4,11 +4,11 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier {
}
class TestMouseZoneManager implements IMouseZoneManager {
dispose(): void {
}
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
+2 -2
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
/**
+61 -2
View File
@@ -4,8 +4,8 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './utils/CharMeasure';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
@@ -25,6 +25,8 @@ class TestSelectionManager extends SelectionManager {
public get model(): SelectionModel { return this._model; }
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
@@ -378,4 +380,61 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.hasSelection, true);
});
});
describe('column selection', () => {
it('should select a column of text', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('abcdefghij'));
buffer.lines.set(1, stringToRow('klmnopqrst'));
buffer.lines.set(2, stringToRow('uvwxyz'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [2, 0];
selectionManager.model.selectionEnd = [4, 2];
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
});
it('should select a column of text without chopping up double width characters', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('語'));
buffer.lines.set(2, stringToRow('b'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n語\nb');
});
it('should select a column of text with single character emojis', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('☃'));
buffer.lines.set(2, stringToRow('c'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
});
it('should select a column of text with double character emojis', () => {
// TODO the case this is testing works for me in the demo webapp,
// but doing it programmatically fails.
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a '));
buffer.lines.set(1, stringArrayToRow(['😁', ' ']));
buffer.lines.set(2, stringToRow('c '));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
});
});
});
+61 -26
View File
@@ -6,7 +6,7 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -54,10 +54,11 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
const enum SelectionMode {
export const enum SelectionMode {
NORMAL,
WORD,
LINE
LINE,
COLUMN
}
/**
@@ -80,7 +81,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
/**
* The current selection mode.
*/
private _activeSelectionMode: SelectionMode;
protected _activeSelectionMode: SelectionMode;
/**
* A setInterval timer that is active while the mouse is down whose callback
@@ -116,6 +117,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
@@ -177,30 +183,43 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return '';
}
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
const result: string[] = [];
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
if (this._activeSelectionMode === SelectionMode.COLUMN) {
// Ignore zero width selections
if (start[0] === end[0]) {
return '';
}
for (let i = start[1]; i <= end[1]; i++) {
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
result.push(lineText);
}
}
} else {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
}
@@ -249,7 +268,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
this.emit('refresh', {
start: this._model.finalSelectionStart,
end: this._model.finalSelectionEnd,
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
});
}
/**
@@ -358,7 +381,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
public shouldForceSelection(event: MouseEvent): boolean {
return Browser.isMac ? event.altKey : event.shiftKey;
if (Browser.isMac) {
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
}
return event.shiftKey;
}
/**
@@ -449,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _onSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = SelectionMode.NORMAL;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
// Initialize the new selection
this._model.selectionStart = this._getMouseBufferCoords(event);
@@ -502,6 +529,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
}
/**
* Returns whether the selection manager should operate in column select mode
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
}
/**
* Handles the mousemove event when the mouse button is down, recording the
* end of the selection and refreshing the selection.
+1 -1
View File
@@ -88,7 +88,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitivePty = pty.native.open(cols, rows);
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', () => {
+11
View File
@@ -120,6 +120,17 @@ describe('term.js addons', () => {
});
});
describe('reset', () => {
it('should not affect cursorState', () => {
term.cursorState = 1;
term.reset();
assert.equal(term.cursorState, 1);
term.cursorState = 0;
term.reset();
assert.equal(term.cursorState, 0);
});
});
describe('clear', () => {
it('should clear a buffer equal to rows', () => {
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
+92 -81
View File
@@ -22,7 +22,7 @@
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
@@ -36,17 +36,17 @@ import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import * as Browser from './shared/utils/Browser';
import * as Dom from './utils/Dom';
import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './input/MouseZoneManager';
import { MouseZoneManager } from './ui/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
import { ITheme, IMarker, IDisposable } from 'xterm';
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
import { DomRenderer } from './renderer/dom/DomRenderer';
@@ -98,6 +98,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
screenReaderMode: false,
debug: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
@@ -113,8 +114,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public element: HTMLElement;
public screenElement: HTMLElement;
private _disposables: IDisposable[];
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
@@ -173,6 +172,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public savedCols: number;
public curAttr: number;
public savedCurAttr: number;
public params: (string | number)[];
public currentParam: string | number;
@@ -234,8 +234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public dispose(): void {
super.dispose();
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
this._customKeyEventHandler = null;
removeTerminalFromCache(this);
this.handler = () => {};
this.write = () => {};
@@ -252,8 +251,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
private _setup(): void {
this._disposables = [];
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key];
@@ -306,6 +303,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._userScrolling = false;
this._inputHandler = new InputHandler(this);
this.register(this._inputHandler);
// Reuse renderer if the Terminal is being recreated via a reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
@@ -445,6 +443,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.charMeasure.measure(this.options);
}
break;
case 'drawBoldTextInBrightColors':
case 'experimentalCharAtlas':
case 'enableBold':
case 'letterSpacing':
@@ -525,30 +524,30 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._bindKeys();
// Bind clipboard functionality
on(this.element, 'copy', (event: ClipboardEvent) => {
this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => {
// If mouse events are active it means the selection manager is disabled and
// copy should be handled by the host program.
if (!this.hasSelection()) {
return;
}
copyHandler(event, this, this.selectionManager);
});
}));
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this);
on(this.textarea, 'paste', pasteHandlerWrapper);
on(this.element, 'paste', pasteHandlerWrapper);
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
// Handle right click context menus
if (Browser.isFirefox) {
// Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
}
});
}));
} else {
on(this.element, 'contextmenu', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
});
}));
}
// Move the textarea under the cursor when middle clicking on Linux to ensure
@@ -557,11 +556,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (Browser.isLinux) {
// Use auxclick event over mousedown the latter doesn't seem to work. Note
// that the regular click event doesn't fire for the middle mouse button.
on(this.element, 'auxclick', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
if (event.button === 1) {
moveTextAreaUnderMouseCursor(event, this.textarea);
}
});
}));
}
}
@@ -570,33 +569,35 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
private _bindKeys(): void {
const self = this;
on(this.element, 'keydown', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keydown', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyDown(ev);
}, true);
}, true));
on(this.element, 'keypress', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keypress', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyPress(ev);
}, true);
}, true));
on(this.element, 'keyup', (ev: KeyboardEvent) => {
this.register(addDisposableDomListener(this.element, 'keyup', (ev: KeyboardEvent) => {
if (!wasMondifierKeyOnlyEvent(ev)) {
this.focus();
}
}, true);
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true);
on(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart());
on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e));
on(this.textarea, 'compositionend', () => this._compositionHelper.compositionend());
this.on('refresh', () => this._compositionHelper.updateCompositionElements());
this.on('refresh', (data) => this._queueLinkification(data.start, data.end));
self._keyUp(ev);
}, true));
this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements()));
this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end)));
}
/**
@@ -617,6 +618,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Create main element container
this.element = this._document.createElement('div');
@@ -646,7 +648,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.register(this._mouseZoneManager);
this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
@@ -658,8 +661,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.textarea.addEventListener('focus', () => this._onTextAreaFocus());
this.textarea.addEventListener('blur', () => this._onTextAreaBlur());
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._compositionView = document.createElement('div');
@@ -677,37 +680,39 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
this.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this.register(this.viewport);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove()));
this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this.renderer.onBlur()));
this.register(this.addDisposableListener('focus', () => this.renderer.onFocus()));
this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged());
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged()));
this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea()));
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
this.selectionManager.on('newselection', text => {
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode)));
this.register(this.selectionManager.addDisposableListener('newselection', text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
// only on Linux to enable middle click to paste selection.
this.textarea.value = text;
this.textarea.focus();
this.textarea.select();
});
this.on('scroll', () => {
}));
this.register(this.addDisposableListener('scroll', () => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
});
this._viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this.renderer);
@@ -977,7 +982,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return button;
}
on(el, 'mousedown', (ev: MouseEvent) => {
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
// Prevent the focus on the textarea from getting lost
// and make sure we get focused on mousedown
@@ -1015,7 +1020,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
sendMove(event);
};
on(this._document, 'mousemove', moveHandler);
// TODO: these event listeners should be managed by the disposable, the Terminal reference may
// be kept aroud if Terminal.dispose is fired when the mouse is down
this._document.addEventListener('mousemove', moveHandler);
}
// x10 compatibility mode can't send button releases
@@ -1026,22 +1033,22 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (moveHandler) {
// Even though this should only be attached when this.normalMouse is true, holding the
// mouse button down when normalMouse changes can happen. Just always try to remove it.
off(this._document, 'mousemove', moveHandler);
this._document.removeEventListener('mousemove', moveHandler);
moveHandler = null;
}
off(this._document, 'mouseup', handler);
this._document.removeEventListener('mouseup', handler);
return this.cancel(ev);
};
on(this._document, 'mouseup', handler);
this._document.addEventListener('mouseup', handler);
return this.cancel(ev);
});
}));
// if (this.normalMouse) {
// on(this.document, 'mousemove', sendMove);
// }
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (!this.mouseEvents) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -1066,27 +1073,27 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this.x10Mouse || this._vt300Mouse || this._decLocator) return;
sendButton(ev);
ev.preventDefault();
});
}));
// allow wheel scrolling in
// the shell for example
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (this.mouseEvents) return;
this.viewport.onWheel(ev);
return this.cancel(ev);
});
}));
on(el, 'touchstart', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchStart(ev);
return this.cancel(ev);
});
}));
on(el, 'touchmove', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchMove(ev);
return this.cancel(ev);
});
}));
}
/**
@@ -1112,6 +1119,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
/**
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) {
this.element.classList.add('xterm-cursor-crosshair');
} else {
this.element.classList.remove('xterm-cursor-crosshair');
}
}
/**
* Display the cursor element
*/
@@ -1429,6 +1447,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
const result = evaluateKeyboardEvent(event, this.applicationCursor, this.browser.isMac, this.options.macOptionIsMeta);
this.updateCursorStyle(event);
// if (result.key === C0.DC3) { // XOFF
// this._writeStopped = true;
// } else if (result.key === C0.DC1) { // XON
@@ -1500,6 +1520,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
protected _keyUp(ev: KeyboardEvent): void {
this.updateCursorStyle(ev);
}
/**
* Handle a keypress event.
* Key Resources:
@@ -1844,9 +1868,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler;
const cursorState = this.cursorState;
this._setup();
this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler;
this.cursorState = cursorState;
this.refresh(0, this.rows - 1);
if (this.viewport) {
this.viewport.syncScrollArea();
@@ -1928,21 +1954,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Helpers
*/
function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean, passive?: boolean): void {
if (!Array.isArray(el)) {
el = [el];
}
el.forEach((element: HTMLElement) => {
element.addEventListener(type, handler, { capture: capture || false, passive: passive || false });
});
}
// TODO: Remove once everything is typed
const on = globalOn;
function off(el: any, type: string, handler: (event: Event) => any, capture: boolean = false): void {
el.removeEventListener(type, handler, capture);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
+4 -4
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -86,7 +86,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
@@ -468,7 +468,7 @@ export interface IDcsHandler {
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser {
export interface IEscapeSequenceParser extends IDisposable {
/**
* Reset the parser to its initial state (handlers are kept).
*/
+7 -3
View File
@@ -5,7 +5,9 @@
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -13,7 +15,7 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
@@ -39,11 +41,13 @@ export class Viewport implements IViewport {
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this._viewportElement.addEventListener('scroll', this._onScroll.bind(this));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
+5 -1
View File
@@ -5,9 +5,13 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;

Some files were not shown because too many files have changed in this diff Show More