Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement selection in the terminal #670

Merged
merged 64 commits into from
Jun 9, 2017
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
70fda99
Basic fetching of coordinates
Tyriar May 6, 2017
b36d878
Clean up
Tyriar May 6, 2017
65256c8
Support an entry ID in CircularList
Tyriar May 6, 2017
fee9120
Merge remote-tracking branch 'ups/master' into 207_selection_manager
Tyriar May 20, 2017
fe11837
Create new entries when shifting
Tyriar May 20, 2017
207c4cf
More work on SelectionManager
Tyriar May 20, 2017
b594407
Tell renderer to draw selection
Tyriar May 20, 2017
ad3ae67
Get selection partially rendering
Tyriar May 20, 2017
2f6d05f
Handle scrolling of selection
Tyriar May 20, 2017
32b34cb
Handle copy
Tyriar May 20, 2017
293ae18
Fix copy after scroll
Tyriar May 20, 2017
597c693
Handle basic double click select
Tyriar May 20, 2017
e63fdf5
Clean up
Tyriar May 20, 2017
a5c48c8
Temp fix until mouse getCoords PR merged
Tyriar May 21, 2017
ab40908
Disable selection manager in mouse mode
Tyriar May 21, 2017
c4762e7
Merge remote-tracking branch 'ups/master' into 207_selection_manager
Tyriar May 23, 2017
0dc3dd0
Initial scroll drag implementation
Tyriar May 23, 2017
b3b2bd1
Don't flag user scrolling if no scrolling happens
Tyriar May 25, 2017
3846fe0
Add null checks to fix trim related crash
Tyriar May 25, 2017
25152e4
Add select all API
Tyriar May 25, 2017
43c796a
Support copying of select all text
Tyriar May 25, 2017
9f271de
Implement triple click to select line
Tyriar May 25, 2017
e29ab29
Fix drag when selectioning via double/triple click
Tyriar May 25, 2017
0716cff
Tidy up _areSelectionValuesReversed
Tyriar May 25, 2017
f7d6ab5
Move the selection model to its own module
Tyriar May 25, 2017
24bed01
Support copy and paste via context menu
Tyriar May 25, 2017
910aa1a
Properly prepare text for clipboard for context menu
Tyriar May 26, 2017
ec61f3a
Only trim the right whitespace from selection
Tyriar May 26, 2017
54e7f65
Support selecting wide characters
Tyriar May 26, 2017
cb6533f
Get double click selection for wide chars mostly working
Tyriar May 26, 2017
d9991b2
Support wide char double click
Tyriar May 26, 2017
11cec31
Clean up wide char select word code
Tyriar May 26, 2017
a53a0ac
Fix issue with trimming whitespace
Tyriar Jun 2, 2017
8b1067d
Remove no longer valid clipboard test and add new one
Tyriar Jun 2, 2017
d3865ad
Add some SelectionManager selectWorkAt tests, fix lint
Tyriar Jun 2, 2017
13c401c
Only refresh the selection on an animation frame
Tyriar Jun 6, 2017
db8ded2
Implement shift+click
Tyriar Jun 6, 2017
59cc0b6
Merge remote-tracking branch 'ups/master' into 207_selection_manager
Tyriar Jun 6, 2017
5bc1112
Select lines when dragging a triple click
Tyriar Jun 6, 2017
f61d852
Don't allow double click selection on empty row going out of viewport
Tyriar Jun 6, 2017
fd91c5e
Add _selectLineAt test
Tyriar Jun 6, 2017
f380153
Resolve some TODOs
Tyriar Jun 7, 2017
2621be8
Resolve more TODOs, add jsdoc
Tyriar Jun 7, 2017
d0b603d
jsdoc all of SelectionManager
Tyriar Jun 7, 2017
ec3bf11
Resolve TODOs
Tyriar Jun 7, 2017
7147787
Undo CiruclarList id changes, fix null check, lint
Tyriar Jun 7, 2017
2b24318
Fix select all with no start and include content below viewport
Tyriar Jun 7, 2017
b812991
Fix multiple drag scroll intervals being registered
Tyriar Jun 6, 2017
9a86eeb
Support copying in the alt buffer
Tyriar Jun 7, 2017
72724ab
Ensure host program handles copy if mouseevents are active
Tyriar Jun 7, 2017
9c459aa
Explicitly size selectionContainer to allow for more flexible layouts
Tyriar Jun 7, 2017
9e47ec9
Add getSelectionText API
Tyriar Jun 7, 2017
1343b83
Add hasSelection public API
Tyriar Jun 7, 2017
5d33727
Add clearSelection API
Tyriar Jun 7, 2017
f719a4e
Fix tests
Tyriar Jun 7, 2017
2012c8c
Move prepareTextForClipboard logic into SelectionManager
Tyriar Jun 7, 2017
4405f5e
Fix clearSelection in select all mode
Tyriar Jun 7, 2017
b81c165
Add SelectionModel tests
Tyriar Jun 7, 2017
a047359
Add more selection manager/model tests
Tyriar Jun 7, 2017
28ed777
Select last character in bottom right in select all
Tyriar Jun 7, 2017
d56a611
Add .gitattributes file
Tyriar Jun 8, 2017
e88a9ff
Fix bug when selection end is less than start
Tyriar Jun 8, 2017
264757e
Use user-select supported by other browsers
Tyriar Jun 8, 2017
6075498
Add class header comments
Tyriar Jun 9, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
11 changes: 8 additions & 3 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface IBrowser {
export interface ITerminal {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
ybase: number;
ydisp: number;
Expand Down Expand Up @@ -47,6 +49,10 @@ export interface ITerminal {
emit(event: string, data: any);
}

export interface ISelectionManager {
selectionText: string;
}

export interface ICharMeasure {
width: number;
height: number;
Expand Down
61 changes: 61 additions & 0 deletions src/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}


Expand Down
169 changes: 169 additions & 0 deletions src/SelectionManager.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Loading