Merge remote-tracking branch 'origin/master' into set_row_height_explicitly

This commit is contained in:
Daniel Imms
2017-06-10 10:21:20 -07:00
16 changed files with 1314 additions and 157 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+8 -3
View File
@@ -75,8 +75,9 @@ export class InputHandler implements IInputHandler {
const removed = this._terminal.lines.get(this._terminal.y + this._terminal.ybase).pop();
if (removed[2] === 0
&& this._terminal.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2)
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2) {
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
}
// insert empty cell at cursor
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
@@ -903,7 +904,8 @@ export class InputHandler implements IInputHandler {
this._terminal.vt200Mouse = params[0] === 1000;
this._terminal.normalMouse = params[0] > 1000;
this._terminal.mouseEvents = true;
this._terminal.element.style.cursor = 'default';
this._terminal.element.classList.add('enable-mouse-events');
this._terminal.selectionManager.disable();
this._terminal.log('Binding to mouse events.');
break;
case 1004: // send focusin/focusout events
@@ -1096,7 +1098,8 @@ export class InputHandler implements IInputHandler {
this._terminal.vt200Mouse = false;
this._terminal.normalMouse = false;
this._terminal.mouseEvents = false;
this._terminal.element.style.cursor = '';
this._terminal.element.classList.remove('enable-mouse-events');
this._terminal.selectionManager.enable();
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
@@ -1127,6 +1130,8 @@ export class InputHandler implements IInputHandler {
this._terminal.scrollBottom = this._terminal.normal.scrollBottom;
this._terminal.tabs = this._terminal.normal.tabs;
this._terminal.normal = null;
// Ensure the selection manager has the correct buffer
this._terminal.selectionManager.setBuffer(this._terminal.lines);
// if (params === 1049) {
// this.x = this.savedX;
// this.y = this.savedY;
+6
View File
@@ -20,6 +20,8 @@ export interface IBrowser {
export interface ITerminal {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
ybase: number;
ydisp: number;
@@ -47,6 +49,10 @@ export interface ITerminal {
emit(event: string, data: any);
}
export interface ISelectionManager {
selectionText: string;
}
export interface ICharMeasure {
width: number;
height: number;
+61
View File
@@ -318,6 +318,67 @@ export class Renderer {
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
};
/**
* Refreshes the selection in the DOM.
* @param start The selection start.
* @param end The selection end.
*/
public refreshSelection(start: [number, number], end: [number, number]) {
// Remove all selections
while (this._terminal.selectionContainer.children.length) {
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
}
// Selection does not exist
if (!start || !end) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.ydisp;
const viewportEndRow = end[1] - this._terminal.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = document.createDocumentFragment();
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
for (let i = viewportCappedStartRow + 1; i < viewportCappedEndRow; i++) {
documentFragment.appendChild(this._createSelectionElement(i, 0, this._terminal.cols));
}
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
this._terminal.selectionContainer.appendChild(documentFragment);
}
/**
* Creates a selection element at the specified position.
* @param row The row of the selection.
* @param colStart The start column.
* @param colEnd The end columns.
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number): HTMLElement {
const element = document.createElement('div');
element.style.height = `${this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
return element;
}
}
+169
View File
@@ -0,0 +1,169 @@
/**
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: CircularList<any>,
rowContainer: HTMLElement,
charMeasure: CharMeasure
) {
super(terminal, buffer, rowContainer, charMeasure);
}
public get model(): SelectionModel { return this._model; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
// Disable DOM interaction
public enable(): void {}
public disable(): void {}
public refresh(): void {}
}
describe('SelectionManager', () => {
let window: Window;
let document: Document;
let terminal: ITerminal;
let buffer: CircularList<any>;
let rowContainer: HTMLElement;
let selectionManager: TestSelectionManager;
beforeEach(done => {
jsdom.env('', (err, w) => {
window = w;
document = window.document;
buffer = new CircularList<any>(100);
terminal = <any>{ cols: 80, rows: 2 };
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
done();
});
});
function stringToRow(text: string): [number, string, number][] {
let result: [number, string, number][] = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1]);
}
return result;
}
describe('_selectWordAt', () => {
it('should expand selection for normal width chars', () => {
buffer.push(stringToRow('foo bar'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, 'bar');
selectionManager.selectWordAt([5, 0]);
assert.equal(selectionManager.selectionText, 'bar');
selectionManager.selectWordAt([6, 0]);
assert.equal(selectionManager.selectionText, 'bar');
});
it('should expand selection for whitespace', () => {
buffer.push(stringToRow('a b'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'a');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, 'b');
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
buffer.push([
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, ' ', 1],
[null, 'a', 1],
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, 'b', 1],
[null, ' ', 1],
[null, 'f', 1],
[null, 'o', 1],
[null, 'o', 1]
]);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, ' ');
// Ensure wide characters work when wrapped in normal width characters
selectionManager.selectWordAt([5, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([6, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([7, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([8, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([9, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([10, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([11, 0]);
assert.equal(selectionManager.selectionText, ' ');
// Ensure normal width characters work fine in a line containing wide characters
selectionManager.selectWordAt([12, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([13, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([14, 0]);
assert.equal(selectionManager.selectionText, 'foo');
});
});
describe('_selectLineAt', () => {
it('should select the entire line', () => {
buffer.push(stringToRow('foo bar'));
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
});
});
describe('selectAll', () => {
it('should select the entire buffer, beyond the viewport', () => {
buffer.push(stringToRow('1'));
buffer.push(stringToRow('2'));
buffer.push(stringToRow('3'));
buffer.push(stringToRow('4'));
buffer.push(stringToRow('5'));
selectionManager.selectAll();
terminal.ybase = buffer.length - terminal.rows;
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
});
});
});
File diff suppressed because it is too large Load Diff
+133
View File
@@ -0,0 +1,133 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel';
class TestSelectionModel extends SelectionModel {
constructor(
terminal: ITerminal
) {
super(terminal);
}
public areSelectionValuesReversed(): boolean { return this._areSelectionValuesReversed(); }
}
describe('SelectionManager', () => {
let window: Window;
let document: Document;
let terminal: ITerminal;
let model: TestSelectionModel;
beforeEach(() => {
terminal = <any>{ cols: 80, rows: 2, ybase: 0 };
model = new TestSelectionModel(terminal);
});
describe('clearSelection', () => {
it('should clear the final selection', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 2]);
model.clearSelection();
assert.deepEqual(model.finalSelectionStart, null);
assert.deepEqual(model.finalSelectionEnd, null);
});
});
describe('_areSelectionValuesReversed', () => {
it('should return true when the selection end is before selection start', () => {
model.selectionStart = [1, 0];
model.selectionEnd = [0, 0];
assert.equal(model.areSelectionValuesReversed(), true);
model.selectionStart = [10, 2];
model.selectionEnd = [0, 0];
assert.equal(model.areSelectionValuesReversed(), true);
});
it('should return false when the selection end is after selection start', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [1, 0];
assert.equal(model.areSelectionValuesReversed(), false);
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
assert.equal(model.areSelectionValuesReversed(), false);
});
});
describe('onTrim', () => {
it('should trim a portion of the selection when a part of it is trimmed', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
});
it('should clear selection when it is trimmed in its entirety', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 0];
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, null);
assert.deepEqual(model.finalSelectionEnd, null);
});
});
describe('finalSelectionStart', () => {
it('should return the start of the buffer if select all is active', () => {
model.isSelectAllActive = true;
assert.deepEqual(model.finalSelectionStart, [0, 0]);
});
it('should return selection start if there is no selection end', () => {
model.selectionStart = [2, 2];
assert.deepEqual(model.finalSelectionStart, [2, 2]);
});
it('should return selection end if values are reversed', () => {
model.selectionStart = [2, 2];
model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionStart, [2, 2]);
model.selectionEnd = [1, 2];
assert.deepEqual(model.finalSelectionStart, [1, 2]);
});
});
describe('finalSelectionEnd', () => {
it('should return the end of the buffer if select all is active', () => {
model.isSelectAllActive = true;
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
});
it('should return null if there is no selection start', () => {
assert.equal(model.finalSelectionEnd, null);
model.selectionEnd = [1, 2];
assert.equal(model.finalSelectionEnd, null);
});
it('should return selection start + length if there is no selection end', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection start + length if values are reversed', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [2, 1];
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection start + length if selection end is inside the start selection', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection end if selection end is after selection start + length', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [5, 2];
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
});
});
});
+128
View File
@@ -0,0 +1,128 @@
/**
* @license MIT
*/
import { ITerminal } from './Interfaces';
/**
* Represents a selection within the buffer. This model only cares about column
* and row coordinates, not wide characters.
*/
export class SelectionModel {
/**
* Whether select all is currently active.
*/
public isSelectAllActive: boolean;
/**
* The [x, y] position the selection starts at.
*/
public selectionStart: [number, number];
/**
* The minimal length of the selection from the start position. When double
* clicking on a word, the word will be selected which makes the selection
* start at the start of the word and makes this variable the length.
*/
public selectionStartLength: number;
/**
* The [x, y] position the selection ends at.
*/
public selectionEnd: [number, number];
constructor(
private _terminal: ITerminal
) {
this.clearSelection();
}
/**
* Clears the current selection.
*/
public clearSelection(): void {
this.selectionStart = null;
this.selectionEnd = null;
this.isSelectAllActive = false;
this.selectionStartLength = 0;
}
/**
* The final selection start, taking into consideration select all.
*/
public get finalSelectionStart(): [number, number] {
if (this.isSelectAllActive) {
return [0, 0];
}
if (!this.selectionEnd || !this.selectionStart) {
return this.selectionStart;
}
return this._areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
}
/**
* The final selection end, taking into consideration select all, double click
* word selection and triple click line selection.
*/
public get finalSelectionEnd(): [number, number] {
if (this.isSelectAllActive) {
return [this._terminal.cols, this._terminal.ybase + this._terminal.rows - 1];
}
if (!this.selectionStart) {
return null;
}
// Use the selection start if the end doesn't exist or they're reversed
if (!this.selectionEnd || this._areSelectionValuesReversed()) {
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
}
// Ensure the the word/line is selected after a double/triple click
if (this.selectionStartLength) {
// Select the larger of the two when start and end are on the same line
if (this.selectionEnd[1] === this.selectionStart[1]) {
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
}
}
return this.selectionEnd;
}
/**
* Returns whether the selection start and end are reversed.
*/
protected _areSelectionValuesReversed(): boolean {
const start = this.selectionStart;
const end = this.selectionEnd;
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
}
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
* @return Whether a refresh is necessary.
*/
public onTrim(amount: number): boolean {
// Adjust the selection position based on the trimmed amount.
if (this.selectionStart) {
this.selectionStart[1] -= amount;
}
if (this.selectionEnd) {
this.selectionEnd[1] -= amount;
}
// The selection has moved off the buffer, clear it.
if (this.selectionEnd && this.selectionEnd[1] < 0) {
this.clearSelection();
return true;
}
// If the selection start is trimmed, ensure the start column is 0.
if (this.selectionStart && this.selectionStart[1] < 0) {
this.selectionStart[1] = 0;
}
return false;
}
}
+6
View File
@@ -4,6 +4,7 @@ import { Viewport } from './Viewport';
describe('Viewport', () => {
let terminal;
let viewportElement;
let selectionContainer;
let charMeasure;
let viewport;
let scrollAreaElement;
@@ -20,6 +21,11 @@ describe('Viewport', () => {
style: {
lineHeight: 0
}
},
selectionContainer: {
style: {
height: 0
}
}
};
viewportElement = {
+1
View File
@@ -57,6 +57,7 @@ export class Viewport {
if (rowHeightChanged || viewportHeightChanged) {
this.lastRecordedViewportHeight = this.terminal.rows;
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
}
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
}
-15
View File
@@ -2,21 +2,6 @@ import { assert } from 'chai';
import * as Terminal from '../xterm';
import * as Clipboard from './Clipboard';
describe('evaluateCopiedTextProcessing', function () {
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
let nonBreakingSpace = String.fromCharCode(160),
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
processedText = Clipboard.prepareTextForClipboard(copiedText);
// No trailing spaces
assert.equal(processedText.match(/\s+$/), null);
// No non-breaking space
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',
+30 -92
View File
@@ -5,7 +5,7 @@
* @license MIT
*/
import { ITerminal } from '../Interfaces';
import { ITerminal, ISelectionManager } from '../Interfaces';
interface IWindow extends Window {
clipboardData?: {
@@ -16,28 +16,6 @@ interface IWindow extends Window {
declare var window: IWindow;
/**
* Prepares text copied from terminal selection, to be saved in the clipboard by:
* 1. stripping all trailing white spaces
* 2. converting all non-breaking spaces to regular spaces
* @param {string} text The copied text that needs processing for storing in clipboard
* @returns {string}
*/
export function prepareTextForClipboard(text: string): string {
let space = String.fromCharCode(32),
nonBreakingSpace = String.fromCharCode(160),
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
processedText = text.split('\n').map(function (line) {
// Strip all trailing white spaces and convert all non-breaking spaces
// to regular spaces.
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
return processedLine;
}).join('\n');
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
@@ -53,19 +31,15 @@ export function prepareTextForTerminal(text: string, isMSWindows: boolean): stri
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
*/
export function copyHandler(ev: ClipboardEvent, term: ITerminal) {
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
// property that we use below for Internet Explorer.
let copiedText = window.getSelection().toString(),
text = prepareTextForClipboard(copiedText);
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) {
if (term.browser.isMSIE) {
window.clipboardData.setData('Text', text);
window.clipboardData.setData('Text', selectionManager.selectionText);
} else {
ev.clipboardData.setData('text/plain', text);
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
}
ev.preventDefault(); // Prevent or the original text will be copied.
// Prevent or the original text will be copied.
ev.preventDefault();
}
/**
@@ -102,67 +76,31 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
/**
* Bind to right-click event and allow right-click copy and paste.
*
* **Logic**
* If text is selected and right-click happens on selected text, then
* do nothing to allow seamless copying.
* If no text is selected or right-click is outside of the selection
* area, then bring the terminal's input below the cursor, in order to
* trigger the event on the textarea and allow-right click paste, without
* caring about disappearing selection.
* @param {MouseEvent} ev The original right click event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
* @param ev The original right click event to be handled
* @param term The terminal on which to apply the handled paste event
* @param selectionManager The terminal's selection manager.
*/
export function rightClickHandler(ev: MouseEvent, term: ITerminal) {
let s = document.getSelection(),
selectedText = prepareTextForClipboard(s.toString()),
clickIsOnSelection = false,
x = ev.clientX,
y = ev.clientY;
if (s.rangeCount) {
let r = s.getRangeAt(0),
cr = r.getClientRects();
for (let i = 0; i < cr.length; i++) {
let rect = cr[i];
clickIsOnSelection = (
(x > rect.left) && (x < rect.right) &&
(y > rect.top) && (y < rect.bottom)
);
if (clickIsOnSelection) {
break;
}
}
// If we clicked on selection and selection is not a single space,
// then mark the right click as copy-only. We check for the single
// space selection, as this can happen when clicking on an &nbsp;
// and there is not much pointing in copying a single space.
if (selectedText.match(/^\s$/) || !selectedText.length) {
clickIsOnSelection = false;
}
}
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
// Bring textarea at the cursor position
if (!clickIsOnSelection) {
term.textarea.style.position = 'fixed';
term.textarea.style.width = '20px';
term.textarea.style.height = '20px';
term.textarea.style.left = (x - 10) + 'px';
term.textarea.style.top = (y - 10) + 'px';
term.textarea.style.zIndex = '1000';
term.textarea.focus();
textarea.style.position = 'fixed';
textarea.style.width = '20px';
textarea.style.height = '20px';
textarea.style.left = (ev.clientX - 10) + 'px';
textarea.style.top = (ev.clientY - 10) + 'px';
textarea.style.zIndex = '1000';
// Reset the terminal textarea's styling
setTimeout(function () {
term.textarea.style.position = null;
term.textarea.style.width = null;
term.textarea.style.height = null;
term.textarea.style.left = null;
term.textarea.style.top = null;
term.textarea.style.zIndex = null;
}, 4);
}
// Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText;
textarea.focus();
textarea.select();
// Reset the terminal textarea's styling
setTimeout(function () {
textarea.style.position = null;
textarea.style.width = null;
textarea.style.height = null;
textarea.style.left = null;
textarea.style.top = null;
textarea.style.zIndex = null;
}, 4);
}
+22 -4
View File
@@ -4,12 +4,15 @@
* @module xterm/utils/CircularList
* @license MIT
*/
export class CircularList<T> {
import { EventEmitter } from '../EventEmitter';
export class CircularList<T> extends EventEmitter {
private _array: T[];
private _startIndex: number;
private _length: number;
constructor(maxLength: number) {
super();
this._array = new Array<T>(maxLength);
this._startIndex = 0;
this._length = 0;
@@ -43,8 +46,14 @@ export class CircularList<T> {
this._length = newLength;
}
public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void {
return this._array.forEach;
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
return (callbackfn: (value: T, index: number) => void) => {
let i = 0;
let length = this.length;
for (let i = 0; i < length; i++) {
callbackfn(this.get(i), i);
}
};
}
/**
@@ -83,6 +92,7 @@ export class CircularList<T> {
if (this._startIndex === this.maxLength) {
this._startIndex = 0;
}
this.emit('trim', 1);
} else {
this._length++;
}
@@ -106,13 +116,16 @@ export class CircularList<T> {
* @param items The items to insert.
*/
public splice(start: number, deleteCount: number, ...items: T[]): void {
// Delete items
if (deleteCount) {
for (let i = start; i < this._length - deleteCount; i++) {
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
}
this._length -= deleteCount;
}
if (items && items.length) {
// Add items
for (let i = this._length - 1; i >= start; i--) {
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
}
@@ -120,9 +133,12 @@ export class CircularList<T> {
this._array[this._getCyclicIndex(start + i)] = items[i];
}
// Adjust length as needed
if (this._length + items.length > this.maxLength) {
this._startIndex += (this._length + items.length) - this.maxLength;
const countToTrim = (this._length + items.length) - this.maxLength;
this._startIndex += countToTrim;
this._length = this.maxLength;
this.emit('trim', countToTrim);
} else {
this._length += items.length;
}
@@ -139,6 +155,7 @@ export class CircularList<T> {
}
this._startIndex += count;
this._length -= count;
this.emit('trim', count);
}
public shiftElements(start: number, count: number, offset: number): void {
@@ -162,6 +179,7 @@ export class CircularList<T> {
while (this._length > this.maxLength) {
this._length--;
this._startIndex++;
this.emit('trim', 1);
}
}
} else {
+29 -25
View File
@@ -4,6 +4,25 @@
import { CharMeasure } from './CharMeasure';
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
return null;
}
let x = event.pageX;
let y = event.pageY;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (element && element !== self.document.documentElement) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
}
return [x, y];
}
/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
@@ -12,29 +31,18 @@ import { CharMeasure } from './CharMeasure';
* @param rowContainer The terminal's row container.
* @param charMeasure The char measure object used to determine character sizes.
*/
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
return null;
}
let x = event.pageX;
let y = event.pageY;
let el = rowContainer;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (el && el !== self.document.documentElement) {
x -= el.offsetLeft;
y -= el.offsetTop;
el = 'offsetParent' in el ? <HTMLElement>el.offsetParent : <HTMLElement>el.parentElement;
}
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): [number, number] {
const coords = getCoordsRelativeToElement(event, rowContainer);
// Convert to cols/rows
x = Math.ceil(x / charMeasure.width);
y = Math.ceil(y / charMeasure.height);
coords[0] = Math.ceil(coords[0] / charMeasure.width);
coords[1] = Math.ceil(coords[1] / charMeasure.height);
return [x, y];
// Ensure coordinates are within the terminal viewport.
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
return coords;
}
/**
@@ -48,14 +56,10 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas
* @param rowCount The number of rows in the terminal.
*/
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
const coords = getCoords(event, rowContainer, charMeasure);
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
let x = coords[0];
let y = coords[1];
// Ensure coordinates are within the terminal viewport.
x = Math.min(Math.max(x, 0), colCount);
y = Math.min(Math.max(y, 0), rowCount);
// xterm sends raw bytes and starts at 32 (SP) for each.
x += 32;
y += 32;
+19
View File
@@ -41,6 +41,9 @@
font-family: courier-new, courier, monospace;
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.terminal.focus,
@@ -180,6 +183,22 @@
left: -9999em;
}
.terminal.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.terminal .xterm-selection {
position: absolute;
top: 0;
left: 0;
}
.terminal .xterm-selection div {
position: absolute;
background-color: #555;
}
/*
* Determine default colors for xterm.js
*/
+70 -18
View File
@@ -20,9 +20,10 @@ import { InputHandler } from './InputHandler';
import { Parser } from './Parser';
import { Renderer } from './Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
import * as Keyboard from './utils/Keyboard';
import * as Mouse from './utils/Mouse';
import { CHARSETS } from './Charsets';
import { getRawByteCoords } from './utils/Mouse';
@@ -220,6 +221,7 @@ function Terminal(options) {
this.parser = new Parser(this.inputHandler, this);
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier();
// user input states
@@ -249,6 +251,10 @@ function Terminal(options) {
while (i--) {
this.lines.push(this.blankLine());
}
// Ensure the selection manager has the correct buffer
if (this.selectionManager) {
this.selectionManager.setBuffer(this.lines);
}
this.tabs;
this.setupStops();
@@ -519,28 +525,28 @@ Terminal.prototype.initGlobal = function() {
Terminal.bindBlur(this);
// Bind clipboard functionality
on(this.element, 'copy', function (ev) {
copyHandler.call(this, ev, term);
on(this.element, 'copy', event => {
// If mouse events are active it means the selection manager is disabled and
// copy should be handled by the host program.
if (this.mouseEvents) {
return;
}
copyHandler(event, term, this.selectionManager);
});
on(this.textarea, 'paste', function (ev) {
pasteHandler.call(this, ev, term);
});
on(this.element, 'paste', function (ev) {
pasteHandler.call(this, ev, term);
});
function rightClickHandlerWrapper (ev) {
rightClickHandler.call(this, ev, term);
}
const pasteHandlerWrapper = event => pasteHandler(event, term);
on(this.textarea, 'paste', pasteHandlerWrapper);
on(this.element, 'paste', pasteHandlerWrapper);
if (term.browser.isFirefox) {
on(this.element, 'mousedown', function (ev) {
on(this.element, 'mousedown', event => {
if (ev.button == 2) {
rightClickHandlerWrapper(ev);
rightClickHandler(event, this.textarea, this.selectionManager);
}
});
} else {
on(this.element, 'contextmenu', rightClickHandlerWrapper);
on(this.element, 'contextmenu', event => {
rightClickHandler(event, this.textarea, this.selectionManager);
});
}
};
@@ -641,6 +647,12 @@ Terminal.prototype.open = function(parent, focus) {
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
// Create the selection container. This needs to be added before the
// rowContainer as the selection must be below the text.
this.selectionContainer = document.createElement('div');
this.selectionContainer.classList.add('xterm-selection');
this.element.appendChild(this.selectionContainer);
// Create the container that will hold the lines of the terminal and then
// produce the lines the lines.
this.rowContainer = document.createElement('div');
@@ -684,12 +696,16 @@ Terminal.prototype.open = function(parent, focus) {
this.charMeasure = new CharMeasure(document, this.helperContainer);
this.charMeasure.on('charsizechanged', function () {
self.updateCharSizeCSS();
self.updateCharSizeStyles();
});
this.charMeasure.measure();
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
this.renderer = new Renderer(this);
this.selectionManager = new SelectionManager(this, this.lines, this.rowContainer, this.charMeasure);
this.selectionManager.on('refresh', data => this.renderer.refreshSelection(data.start, data.end));
this.on('scroll', () => this.selectionManager.refresh());
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
// Setup loop that draws to screen
this.refresh(0, this.rows - 1);
@@ -760,7 +776,7 @@ 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() {
Terminal.prototype.updateCharSizeStyles = function() {
this.charSizeStyleElement.textContent =
`.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
`.xterm-normal-char{width:${this.charMeasure.width}px;}` +
@@ -1167,6 +1183,9 @@ Terminal.prototype.scroll = function() {
*/
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
if (disp < 0) {
if (this.ydisp === 0) {
return;
}
this.userScrolling = true;
} else if (disp + this.ydisp >= this.ybase) {
this.userScrolling = false;
@@ -1355,6 +1374,35 @@ Terminal.prototype.deregisterLinkMatcher = function(matcherId) {
}
}
/**
* Gets whether the terminal has an active selection.
*/
Terminal.prototype.hasSelection = function() {
return this.selectionManager.hasSelection;
}
/**
* Gets the terminal's current selection, this is useful for implementing copy
* behavior outside of xterm.js.
*/
Terminal.prototype.getSelection = function() {
return this.selectionManager.selectionText;
}
/**
* Clears the current terminal selection.
*/
Terminal.prototype.clearSelection = function() {
this.selectionManager.clearSelection();
}
/**
* Selects all text within the terminal.
*/
Terminal.prototype.selectAll = function() {
this.selectionManager.selectAll();
}
/**
* Handle a keydown event
* Key Resources:
@@ -1686,6 +1734,10 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) {
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
result.key = C0.ESC + (ev.keyCode - 48);
}
} else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a
this.selectAll();
}
}
break;
}