Merge remote-tracking branch 'upstream/master' into 335_viewport_var

This commit is contained in:
Daniel Imms
2017-01-08 17:04:18 -08:00
10 changed files with 190 additions and 114 deletions
+5
View File
@@ -26,6 +26,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
@@ -117,6 +118,10 @@ Visit https://lair.io/sourcelair/xterm and follow the instructions. All developm
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
#### [Eclipse Che](http://www.eclipse.org/che)
You can start Eclipse Che with `docker run eclipse/che start`
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
+3 -1
View File
@@ -11,7 +11,9 @@ export class EventEmitter {
private _events: {[type: string]: ListenerType[]};
constructor() {
this._events = {};
// 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 || {};
}
public on(type, listener): void {
+10 -16
View File
@@ -2,11 +2,11 @@ import { assert } from 'chai';
import { Viewport } from './Viewport';
describe('Viewport', () => {
var terminal;
var viewportElement;
var charMeasureElement;
var viewport;
var scrollAreaElement;
let terminal;
let viewportElement;
let charMeasure;
let viewport;
let scrollAreaElement;
const CHARACTER_HEIGHT = 10;
@@ -34,21 +34,17 @@ describe('Viewport', () => {
height: 0
}
};
charMeasureElement = {
getBoundingClientRect: () => {
return { width: null, height: CHARACTER_HEIGHT };
}
charMeasure = {
height: CHARACTER_HEIGHT
};
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement);
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure);
});
describe('refresh', () => {
it('should set the line-height of the terminal', () => {
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
charMeasureElement.getBoundingClientRect = () => {
return { width: null, height: 1 };
};
charMeasure.height = 1;
viewport.refresh();
assert.equal(viewportElement.style.lineHeight, '1px');
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
@@ -59,9 +55,7 @@ describe('Viewport', () => {
terminal.rows = 1;
viewport.refresh();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
charMeasureElement.getBoundingClientRect = () => {
return { width: null, height: 20 };
};
charMeasure.height = 20;
viewport.refresh();
assert.equal(viewportElement.style.height, 20 + 'px');
});
+12 -13
View File
@@ -3,6 +3,7 @@
*/
import { ITerminal } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
@@ -24,7 +25,7 @@ export class Viewport {
private terminal: ITerminal,
private viewportElement: HTMLElement,
private scrollArea: HTMLElement,
private charMeasureElement: HTMLElement
private charMeasure: CharMeasure
) {
this.currentRowHeight = 0;
this.lastRecordedBufferLength = 0;
@@ -43,21 +44,20 @@ export class Viewport {
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
* be created.
*/
private refresh(charSize?: ClientRect): void {
var size = charSize || this.charMeasureElement.getBoundingClientRect();
if (size.height > 0) {
const rowHeightChanged = size.height !== this.currentRowHeight;
private refresh(): void {
if (this.charMeasure.height > 0) {
const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight;
if (rowHeightChanged) {
this.currentRowHeight = size.height;
this.viewportElement.style.lineHeight = size.height + 'px';
this.terminal.rowContainer.style.lineHeight = size.height + 'px';
this.currentRowHeight = this.charMeasure.height;
this.viewportElement.style.lineHeight = this.charMeasure.height + 'px';
this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px';
}
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
if (rowHeightChanged || viewportHeightChanged) {
this.lastRecordedViewportHeight = this.terminal.rows;
this.viewportElement.style.height = size.height * this.terminal.rows + 'px';
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
}
this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px';
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
}
}
@@ -74,9 +74,8 @@ export class Viewport {
this.refresh();
} else {
// If size has changed, refresh viewport
const size = this.charMeasureElement.getBoundingClientRect();
if (size.height !== this.currentRowHeight) {
this.refresh(size);
if (this.charMeasure.height !== this.currentRowHeight) {
this.refresh();
}
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Attributes and methods to help with identifying the current browser and platform.
* @module xterm/utils/Browser
* @license MIT
*/
import { contains } from './Generic.js';
let isNode = (typeof navigator == 'undefined') ? true : false;
let userAgent = (isNode) ? 'node' : navigator.userAgent;
let platform = (isNode) ? 'node' : navigator.platform;
export let isFirefox = !!~userAgent.indexOf('Firefox');
export let isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
// Find the users platform. We use this to interpret the meta key
// and ISO third level shifts.
// http://stackoverflow.com/q/19877924/577598
export let isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform);
export let isIpad = platform === 'iPad';
export let isIphone = platform === 'iPhone';
export let isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
+22
View File
@@ -0,0 +1,22 @@
/**
* Attributes and methods to help with identifying the current browser and platform.
* @module xterm/utils/Browser
* @license MIT
*/
import { contains } from './Generic';
const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = !!~userAgent.indexOf('Firefox');
export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
// Find the users platform. We use this to interpret the meta key
// and ISO third level shifts.
// http://stackoverflow.com/q/19877924/577598
export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform);
export const isIpad = platform === 'iPad';
export const isIphone = platform === 'iPhone';
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
+52
View File
@@ -0,0 +1,52 @@
/**
* @module xterm/utils/CharMeasure
* @license MIT
*/
import { EventEmitter } from '../EventEmitter.js';
/**
* Utility class that measures the size of a character.
*/
export class CharMeasure extends EventEmitter {
private _parentElement: HTMLElement;
private _measureElement: HTMLElement;
private _width: number;
private _height: number;
constructor(parentElement: HTMLElement) {
super();
this._parentElement = parentElement;
}
public get width(): number {
return this._width;
}
public get height(): number {
return this._height;
}
public measure(): void {
const oldWidth = this._width;
const oldHeight = this._height;
if (!this._measureElement) {
this._measureElement = document.createElement('span');
this._measureElement.style.position = 'absolute';
this._measureElement.style.top = '0';
this._measureElement.style.left = '-9999em';
this._measureElement.textContent = 'W';
}
this._parentElement.appendChild(this._measureElement);
const geometry = this._measureElement.getBoundingClientRect();
this._width = geometry.width;
this._height = geometry.height;
this._parentElement.removeChild(this._measureElement);
if (this._width !== oldWidth || this._height !== oldHeight) {
this.emit('charsizechanged');
}
}
}
@@ -9,6 +9,6 @@
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
export let contains = function(arr, el) {
export function contains(arr: any[], el: any) {
return arr.indexOf(el) >= 0;
};
+4
View File
@@ -116,6 +116,10 @@
overflow-y: scroll;
}
.terminal .xterm-wide-char {
display: inline-block;
}
.terminal .xterm-rows {
position: absolute;
left: 0;
+81 -61
View File
@@ -15,6 +15,7 @@ import { EventEmitter } from './EventEmitter.js';
import { Viewport } from './Viewport.js';
import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js';
import { CircularList } from './utils/CircularList.js';
import { CharMeasure } from './utils/CharMeasure.js';
import * as Browser from './utils/Browser';
import * as Keyboard from './utils/Keyboard';
@@ -137,14 +138,8 @@ function Terminal(options) {
*/
this.y = 0;
/**
* Used to debounce the refresh function
*/
this.isRefreshing = false;
/**
* Whether there is a full terminal refresh queued
*/
/** A queue of the rows to be refreshed */
this.refreshRowsQueue = [];
this.cursorState = 0;
this.cursorHidden = false;
@@ -407,7 +402,7 @@ Terminal.prototype.blur = function() {
*/
Terminal.bindBlur = function (term) {
on(term.textarea, 'blur', function (ev) {
term.refresh(term.y, term.y);
term.queueRefresh(term.y, term.y);
if (term.sendFocus) {
term.send('\x1b[O');
}
@@ -573,20 +568,25 @@ Terminal.prototype.open = function(parent) {
this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this);
this.helperContainer.appendChild(this.compositionView);
this.charMeasureElement = document.createElement('div');
this.charMeasureElement.classList.add('xterm-char-measure-element');
this.charMeasureElement.innerHTML = 'W';
this.helperContainer.appendChild(this.charMeasureElement);
this.charSizeStyleElement = document.createElement('style');
this.helperContainer.appendChild(this.charSizeStyleElement);
for (; i < this.rows; i++) {
this.insertRow();
}
this.parent.appendChild(this.element);
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement);
this.charMeasure = new CharMeasure(this.rowContainer);
this.charMeasure.on('charsizechanged', function () {
self.updateCharSizeCSS();
});
this.charMeasure.measure();
// Draw the screen.
this.refresh(0, this.rows - 1);
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
// Setup loop that draws to screen
this.queueRefresh(0, this.rows - 1);
this.refreshLoop();
// Initialize global actions that
// need to be taken on the document.
@@ -641,6 +641,13 @@ Terminal.loadAddon = function(addon, callback) {
}
};
/**
* Updates the helper CSS class with any changes necessary after the terminal's
* character width has been changed.
*/
Terminal.prototype.updateCharSizeCSS = function() {
this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}';
}
/**
* XTerm mouse events
@@ -997,6 +1004,48 @@ Terminal.flags = {
INVISIBLE: 16
}
/**
* Queues a refresh between two rows (inclusive), to be done on next animation
* frame.
* @param {number} start The start row.
* @param {number} end The end row.
*/
Terminal.prototype.queueRefresh = function(start, end) {
this.refreshRowsQueue.push({ start: start, end: end });
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
Terminal.prototype.refreshLoop = function() {
// Don't refresh if there were no row changes
if (this.refreshRowsQueue.length > 0) {
var start;
var end;
if (this.refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this.refreshRowsQueue[0].start;
end = this.refreshRowsQueue[0].end;
for (var i = 1; i < this.refreshRowsQueue.length; i++) {
if (this.refreshRowsQueue[i].start < start) {
start = this.refreshRowsQueue[i].start;
}
if (this.refreshRowsQueue[i].end > end) {
end = this.refreshRowsQueue[i].end;
}
}
}
this.refreshRowsQueue = [];
this.refresh(start, end);
}
window.requestAnimationFrame(this.refreshLoop.bind(this));
}
/**
* Refreshes (re-renders) terminal content within two rows (inclusive)
*
@@ -1017,47 +1066,10 @@ Terminal.flags = {
*
* @param {number} start The row to start from (between 0 and terminal's height terminal - 1)
* @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
* @param {boolean} queue Whether the refresh should ran right now or be queued
*/
Terminal.prototype.refresh = function(start, end, queue) {
Terminal.prototype.refresh = function(start, end) {
var self = this;
// queue defaults to true
queue = (typeof queue == 'undefined') ? true : queue;
/**
* The refresh queue allows refresh to execute only approximately 30 times a second. For
* commands that pass a significant amount of output to the write function, this prevents the
* terminal from maxing out the CPU and making the UI unresponsive. While commands can still
* run beyond what they do on the terminal, it is far better with a debounce in place as
* every single terminal manipulation does not need to be constructed in the DOM.
*
* A side-effect of this is that it makes ^C to interrupt a process seem more responsive.
*/
if (queue) {
// If refresh should be queued, order the refresh and return.
if (this._refreshIsQueued) {
// If a refresh has already been queued, just order a full refresh next
this._fullRefreshNext = true;
} else {
setTimeout(function () {
self.refresh(start, end, false);
}, 34)
this._refreshIsQueued = true;
}
return;
}
// If refresh should be run right now (not be queued), release the lock
this._refreshIsQueued = false;
// If multiple refreshes were requested, make a full refresh.
if (this._fullRefreshNext) {
start = 0;
end = this.rows - 1;
this._fullRefreshNext = false // reset lock
}
var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;
// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
@@ -1182,6 +1194,9 @@ Terminal.prototype.refresh = function(start, end, queue) {
}
}
if (ch_width === 2) {
out += '<span class="xterm-wide-char">';
}
switch (ch) {
case '&':
out += '&amp;';
@@ -1200,6 +1215,9 @@ Terminal.prototype.refresh = function(start, end, queue) {
}
break;
}
if (ch_width === 2) {
out += '</span>';
}
attr = data;
}
@@ -1224,7 +1242,7 @@ Terminal.prototype.refresh = function(start, end, queue) {
Terminal.prototype.showCursor = function() {
if (!this.cursorState) {
this.cursorState = 1;
this.refresh(this.y, this.y);
this.queueRefresh(this.y, this.y);
}
};
@@ -1311,7 +1329,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
this.emit('scroll', this.ydisp);
}
this.refresh(0, this.rows - 1);
this.queueRefresh(0, this.rows - 1);
};
/**
@@ -2379,7 +2397,7 @@ Terminal.prototype.write = function(data) {
}
this.updateRange(this.y);
this.refresh(this.refreshStart, this.refreshEnd);
this.queueRefresh(this.refreshStart, this.refreshEnd);
};
/**
@@ -2945,7 +2963,9 @@ Terminal.prototype.resize = function(x, y) {
this.scrollTop = 0;
this.scrollBottom = y - 1;
this.refresh(0, this.rows - 1);
this.charMeasure.measure();
this.queueRefresh(0, this.rows - 1);
this.normal = null;
@@ -3074,7 +3094,7 @@ Terminal.prototype.clear = function() {
for (var i = 1; i < this.rows; i++) {
this.lines.push(this.blankLine());
}
this.refresh(0, this.rows - 1);
this.queueRefresh(0, this.rows - 1);
this.emit('scroll', this.ydisp);
};
@@ -3205,7 +3225,7 @@ Terminal.prototype.reset = function() {
var customKeydownHandler = this.customKeydownHandler;
Terminal.call(this, this.options);
this.customKeydownHandler = customKeydownHandler;
this.refresh(0, this.rows - 1);
this.queueRefresh(0, this.rows - 1);
this.viewport.syncScrollArea();
};
@@ -4324,7 +4344,7 @@ Terminal.prototype.resetMode = function(params) {
// this.x = this.savedX;
// this.y = this.savedY;
// }
this.refresh(0, this.rows - 1);
this.queueRefresh(0, this.rows - 1);
this.viewport.syncScrollArea();
this.showCursor();
}