Merge remote-tracking branch 'ups/master' into 207_selection_manager

This commit is contained in:
Daniel Imms
2017-05-19 18:38:24 -07:00
13 changed files with 279 additions and 84 deletions
+4
View File
@@ -24,14 +24,17 @@ Dan Kaplun <dbkaplun@twitch.tv>
Darin Morrison <freebroccolo@users.noreply.github.com>
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
Ian Lewis <ianlewis@google.com>
imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jörg Breitbart <jerch@rockborn.de>
Justin Mecham <justin@mecham.me>
Lucian Buzzo <lucian.buzzo@gmail.com>
Maël Nison <nison.mael@gmail.com>
Martin Chloride <i@martincl2.me>
Martin Wang <jiahaow@ca.ibm.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
@@ -40,6 +43,7 @@ Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
runarberg <runar@greenqloud.com>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
Thanasis Daglis <thanasis@sourcelair.com>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm.js",
"version": "2.5.0",
"version": "2.6.0",
"ignore": ["demo", "test", ".gitignore"],
"main": [
"dist/xterm.js",
+13 -2
View File
@@ -3008,7 +3008,7 @@ Terminal.prototype.insertRow = function (row) {
this.children.push(row);
return row;
};
Terminal.prototype.open = function (parent) {
Terminal.prototype.open = function (parent, focus) {
var self = this, i = 0, div;
this.parent = parent || this.parent;
if (!this.parent) {
@@ -3070,7 +3070,16 @@ Terminal.prototype.open = function (parent) {
this.renderer = new Renderer_1.Renderer(this);
this.refresh(0, this.rows - 1);
this.initGlobal();
this.focus();
if (typeof focus == 'undefined') {
var message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n';
message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 ';
message += 'it will default to `false`.';
console.warn(message);
focus = true;
}
if (focus) {
this.focus();
}
on(this.element, 'click', function () {
var selection = document.getSelection(), collapsed = selection.isCollapsed, isRange = typeof collapsed == 'boolean' ? !collapsed : selection.type == 'Range';
if (!isRange) {
@@ -4091,8 +4100,10 @@ Terminal.prototype.reset = function () {
this.options.rows = this.rows;
this.options.cols = this.cols;
var customKeydownHandler = this.customKeydownHandler;
var cursorBlinkInterval = this.cursorBlinkInterval;
Terminal.call(this, this.options);
this.customKeydownHandler = customKeydownHandler;
this.cursorBlinkInterval = cursorBlinkInterval;
this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea();
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -83,6 +83,7 @@ gulp.task('instrument-test', function () {
gulp.task('mocha', ['instrument-test'], function () {
return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "2.5.0",
"version": "2.6.0",
"ignore": [
"demo",
"test",
+108 -77
View File
@@ -3,6 +3,7 @@
*/
import { ITerminal } from './Interfaces';
import { DomElementObjectPool } from './utils/DomElementObjectPool';
/**
* The maximum number of refresh frames to skip when the write buffer is non-
@@ -30,12 +31,15 @@ export class Renderer {
private _refreshFramesSkipped = 0;
private _refreshAnimationFrame = null;
private _spanElementObjectPool = new DomElementObjectPool('span');
constructor(private _terminal: ITerminal) {
// Figure out whether boldness affects
// the character width of monospace fonts.
if (brokenBold === null) {
brokenBold = checkBoldBroken((<any>this._terminal).element);
}
this._spanElementObjectPool = new DomElementObjectPool('span');
// TODO: Pull more DOM interactions into Renderer.constructor, element for
// example should be owned by Renderer (and also exposed by Terminal due to
@@ -117,9 +121,8 @@ export class Renderer {
* @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
*/
private _refresh(start: number, end: number): void {
let 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
let parent;
if (end - start >= this._terminal.rows / 2) {
parent = this._terminal.element.parentNode;
if (parent) {
@@ -127,8 +130,8 @@ export class Renderer {
}
}
width = this._terminal.cols;
y = start;
let width = this._terminal.cols;
let y = start;
if (end >= this._terminal.rows) {
this._terminal.log('`end` is too large. Most likely a bad CSR.');
@@ -136,80 +139,104 @@ export class Renderer {
}
for (; y <= end; y++) {
row = y + this._terminal.ydisp;
let row = y + this._terminal.ydisp;
line = this._terminal.lines.get(row);
if (!line || !this._terminal.children[y]) {
// Continue if the line is not available, this means a resize is currently in progress
continue;
}
out = '';
let line = this._terminal.lines.get(row);
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp)
&& this._terminal.cursorState
&& !this._terminal.cursorHidden) {
let x;
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) &&
this._terminal.cursorState &&
!this._terminal.cursorHidden) {
x = this._terminal.x;
} else {
x = -1;
}
attr = this._terminal.defAttr;
i = 0;
let attr = this._terminal.defAttr;
for (; i < width; i++) {
if (!line[i]) {
// Continue if the character is not available, this means a resize is currently in progress
const documentFragment = document.createDocumentFragment();
let innerHTML = '';
let currentElement;
// Return the row's spans to the pool
while (this._terminal.children[y].children.length) {
const child = this._terminal.children[y].children[0];
this._terminal.children[y].removeChild(child);
this._spanElementObjectPool.release(<HTMLElement>child);
}
for (let i = 0; i < width; i++) {
// TODO: Could data be a more specific type?
let data: any = line[i][0];
const ch = line[i][1];
const ch_width: any = line[i][2];
if (!ch_width) {
continue;
}
data = line[i][0];
ch = line[i][1];
ch_width = line[i][2];
if (!ch_width)
continue;
if (i === x) data = -1;
if (i === x) {
data = -1;
}
if (data !== attr) {
if (attr !== this._terminal.defAttr) {
out += '</span>';
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
if (data !== this._terminal.defAttr) {
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
}
currentElement = this._spanElementObjectPool.acquire();
if (data === -1) {
out += '<span class="reverse-video terminal-cursor">';
currentElement.classList.add('reverse-video', 'terminal-cursor');
} else {
let classNames = [];
bg = data & 0x1ff;
fg = (data >> 9) & 0x1ff;
flags = data >> 18;
let bg = data & 0x1ff;
let fg = (data >> 9) & 0x1ff;
let flags = data >> 18;
if (flags & FLAGS.BOLD) {
if (!brokenBold) {
classNames.push('xterm-bold');
currentElement.classList.add('xterm-bold');
}
// See: XTerm*boldColors
if (fg < 8) fg += 8;
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
classNames.push('xterm-underline');
currentElement.classList.add('xterm-underline');
}
if (flags & FLAGS.BLINK) {
classNames.push('xterm-blink');
currentElement.classList.add('xterm-blink');
}
// If inverse flag is on, then swap the foreground and background variables.
if (flags & FLAGS.INVERSE) {
/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */
bg = [fg, fg = bg][0];
// Should inverse just be before the
// above boldColors effect instead?
if ((flags & 1) && fg < 8) fg += 8;
let temp = bg;
bg = fg;
fg = temp;
// Should inverse just be before the above boldColors effect instead?
if ((flags & 1) && fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.INVISIBLE) {
classNames.push('xterm-hidden');
currentElement.classList.add('xterm-hidden');
}
/**
@@ -229,55 +256,60 @@ export class Renderer {
}
if (bg < 256) {
classNames.push('xterm-bg-color-' + bg);
currentElement.classList.add(`xterm-bg-color-${bg}`);
}
if (fg < 256) {
classNames.push('xterm-color-' + fg);
currentElement.classList.add(`xterm-color-${fg}`);
}
out += '<span';
if (classNames.length) {
out += ' class="' + classNames.join(' ') + '"';
}
out += '>';
}
}
}
if (ch_width === 2) {
out += '<span class="xterm-wide-char">';
}
switch (ch) {
case '&':
out += '&amp;';
break;
case '<':
out += '&lt;';
break;
case '>':
out += '&gt;';
break;
default:
if (ch <= ' ') {
out += '&nbsp;';
} else {
out += ch;
}
break;
}
if (ch_width === 2) {
out += '</span>';
// Wrap wide characters so they're sized correctly. It's more difficult to release these
// from the object pool so just create new ones via innerHTML.
innerHTML += `<span class="xterm-wide-char">${ch}</span>`;
} else if (ch.charCodeAt(0) > 255) {
// Wrap any non-wide unicode character as some fonts size them badly
innerHTML += `<span class="xterm-normal-char">${ch}</span>`;
} else {
switch (ch) {
case '&':
innerHTML += '&amp;';
break;
case '<':
innerHTML += '&lt;';
break;
case '>':
innerHTML += '&gt;';
break;
default:
if (ch <= ' ') {
innerHTML += '&nbsp;';
} else {
innerHTML += ch;
}
break;
}
}
attr = data;
}
if (attr !== this._terminal.defAttr) {
out += '</span>';
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
this._terminal.children[y].innerHTML = out;
this._terminal.children[y].appendChild(documentFragment);
}
if (parent) {
@@ -289,8 +321,7 @@ export class Renderer {
}
// if bold is broken, we can't
// use it in the terminal.
// If bold is broken, we can't use it in the terminal.
function checkBoldBroken(terminal) {
const document = terminal.ownerDocument;
const el = document.createElement('span');
+11
View File
@@ -16,3 +16,14 @@ describe('evaluateCopiedTextProcessing', function () {
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
});
});
describe('evaluatePastedTextProcessing', function () {
it('should replace carriage return + line feed with line feed on windows', function () {
const pastedText = 'foo\r\nbar\r\n',
processedText = Clipboard.prepareTextForTerminal(pastedText, false),
windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\nbar\n');
});
});
+12
View File
@@ -38,6 +38,17 @@ export function prepareTextForClipboard(text: string): string {
return processedText;
}
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
if (isMSWindows) {
return text.replace(/\r?\n/g, '\n');
}
return text;
}
/**
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
@@ -68,6 +79,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
let text: string;
let dispatchPaste = function(text) {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
term.handler(text);
term.textarea.value = '';
return term.cancel(ev);
+47
View File
@@ -0,0 +1,47 @@
import { assert } from 'chai';
import { DomElementObjectPool } from './DomElementObjectPool';
class MockDocument {
private _attr: {[key: string]: string} = {};
constructor() {}
public getAttribute(key: string): string { return this._attr[key]; };
public setAttribute(key: string, value: string): void { this._attr[key] = value; }
}
describe('DomElementObjectPool', () => {
let pool: DomElementObjectPool;
beforeEach(() => {
pool = new DomElementObjectPool('span');
(<any>global).document = {
createElement: () => new MockDocument()
};
});
it('should acquire distinct elements', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
assert.notEqual(element1, element2);
});
it('should acquire released elements', () => {
const element = pool.acquire();
pool.release(element);
assert.equal(pool.acquire(), element);
});
it('should handle a series of acquisitions and releases', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
pool.release(element1);
assert.equal(pool.acquire(), element1);
pool.release(element1);
pool.release(element2);
assert.equal(pool.acquire(), element2);
assert.equal(pool.acquire(), element1);
});
it('should throw when releasing an element that was not acquired', () => {
assert.throws(() => pool.release(document.createElement('span')));
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* @module xterm/utils/DomElementObjectPool
* @license MIT
*/
/**
* An object pool that manages acquisition and releasing of DOM elements for
* when reuse is desirable.
*/
export class DomElementObjectPool {
private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id';
private static _objectCount = 0;
private _type: string;
private _pool: HTMLElement[];
private _inUse: {[key: string]: HTMLElement};
/**
* @param type The DOM element type (div, span, etc.).
*/
constructor(private type: string) {
this._type = type;
this._pool = [];
this._inUse = {};
}
/**
* Acquire an element from the pool, creating it if the pool is empty.
*/
public acquire(): HTMLElement {
let element: HTMLElement;
if (this._pool.length === 0) {
element = this._createNew();
} else {
element = this._pool.pop();
}
this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element;
return element;
}
/**
* Release an element back into the pool. It's up to the caller of this
* function to ensure that all external references to the element have been
* removed.
* @param element The element being released.
*/
public release(element: HTMLElement): void {
if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) {
throw new Error('Could not release an element not yet acquired');
}
delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)];
this._cleanElement(element);
this._pool.push(element);
}
/**
* Creates a new element for the pool.
*/
private _createNew(): HTMLElement {
const element = document.createElement(this._type);
const id = DomElementObjectPool._objectCount++;
element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10));
return element;
}
/**
* Resets an element back to a "clean state".
* @param element The element to be cleaned.
*/
private _cleanElement(element: HTMLElement): void {
element.className = '';
element.innerHTML = '';
}
}
+2 -1
View File
@@ -154,7 +154,8 @@
overflow-y: scroll;
}
.terminal .xterm-wide-char {
.terminal .xterm-wide-char,
.terminal .xterm-normal-char {
display: inline-block;
}
+3 -1
View File
@@ -763,7 +763,9 @@ Terminal.loadAddon = function(addon, callback) {
* character width has been changed.
*/
Terminal.prototype.updateCharSizeCSS = function() {
this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}';
this.charSizeStyleElement.textContent =
`.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
`.xterm-normal-char{width:${this.charMeasure.width}px;}`
}
/**