Skip to content

Commit a889fef

Browse files
authored
Merge pull request #670 from Tyriar/207_selection_manager
Reimplement selection in the terminal
2 parents 4b72791 + 6075498 commit a889fef

16 files changed

+1313
-156
lines changed

Diff for: .gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

Diff for: src/InputHandler.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ export class InputHandler implements IInputHandler {
7575
const removed = this._terminal.lines.get(this._terminal.y + this._terminal.ybase).pop();
7676
if (removed[2] === 0
7777
&& this._terminal.lines.get(row)[this._terminal.cols - 2]
78-
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2)
78+
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2) {
7979
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
80+
}
8081

8182
// insert empty cell at cursor
8283
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
@@ -903,7 +904,8 @@ export class InputHandler implements IInputHandler {
903904
this._terminal.vt200Mouse = params[0] === 1000;
904905
this._terminal.normalMouse = params[0] > 1000;
905906
this._terminal.mouseEvents = true;
906-
this._terminal.element.style.cursor = 'default';
907+
this._terminal.element.classList.add('enable-mouse-events');
908+
this._terminal.selectionManager.disable();
907909
this._terminal.log('Binding to mouse events.');
908910
break;
909911
case 1004: // send focusin/focusout events
@@ -1096,7 +1098,8 @@ export class InputHandler implements IInputHandler {
10961098
this._terminal.vt200Mouse = false;
10971099
this._terminal.normalMouse = false;
10981100
this._terminal.mouseEvents = false;
1099-
this._terminal.element.style.cursor = '';
1101+
this._terminal.element.classList.remove('enable-mouse-events');
1102+
this._terminal.selectionManager.enable();
11001103
break;
11011104
case 1004: // send focusin/focusout events
11021105
this._terminal.sendFocus = false;
@@ -1127,6 +1130,8 @@ export class InputHandler implements IInputHandler {
11271130
this._terminal.scrollBottom = this._terminal.normal.scrollBottom;
11281131
this._terminal.tabs = this._terminal.normal.tabs;
11291132
this._terminal.normal = null;
1133+
// Ensure the selection manager has the correct buffer
1134+
this._terminal.selectionManager.setBuffer(this._terminal.lines);
11301135
// if (params === 1049) {
11311136
// this.x = this.savedX;
11321137
// this.y = this.savedY;

Diff for: src/Interfaces.ts

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface IBrowser {
2020
export interface ITerminal {
2121
element: HTMLElement;
2222
rowContainer: HTMLElement;
23+
selectionContainer: HTMLElement;
24+
charMeasure: ICharMeasure;
2325
textarea: HTMLTextAreaElement;
2426
ybase: number;
2527
ydisp: number;
@@ -47,6 +49,10 @@ export interface ITerminal {
4749
emit(event: string, data: any);
4850
}
4951

52+
export interface ISelectionManager {
53+
selectionText: string;
54+
}
55+
5056
export interface ICharMeasure {
5157
width: number;
5258
height: number;

Diff for: src/Renderer.ts

+61
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,67 @@ export class Renderer {
318318

319319
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
320320
};
321+
322+
/**
323+
* Refreshes the selection in the DOM.
324+
* @param start The selection start.
325+
* @param end The selection end.
326+
*/
327+
public refreshSelection(start: [number, number], end: [number, number]) {
328+
// Remove all selections
329+
while (this._terminal.selectionContainer.children.length) {
330+
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
331+
}
332+
333+
// Selection does not exist
334+
if (!start || !end) {
335+
return;
336+
}
337+
338+
// Translate from buffer position to viewport position
339+
const viewportStartRow = start[1] - this._terminal.ydisp;
340+
const viewportEndRow = end[1] - this._terminal.ydisp;
341+
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
342+
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
343+
344+
// No need to draw the selection
345+
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
346+
return;
347+
}
348+
349+
// Create the selections
350+
const documentFragment = document.createDocumentFragment();
351+
// Draw first row
352+
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
353+
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
354+
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
355+
// Draw middle rows
356+
for (let i = viewportCappedStartRow + 1; i < viewportCappedEndRow; i++) {
357+
documentFragment.appendChild(this._createSelectionElement(i, 0, this._terminal.cols));
358+
}
359+
// Draw final row
360+
if (viewportCappedStartRow !== viewportCappedEndRow) {
361+
// Only draw viewportEndRow if it's not the same as viewporttartRow
362+
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
363+
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
364+
}
365+
this._terminal.selectionContainer.appendChild(documentFragment);
366+
}
367+
368+
/**
369+
* Creates a selection element at the specified position.
370+
* @param row The row of the selection.
371+
* @param colStart The start column.
372+
* @param colEnd The end columns.
373+
*/
374+
private _createSelectionElement(row: number, colStart: number, colEnd: number): HTMLElement {
375+
const element = document.createElement('div');
376+
element.style.height = `${this._terminal.charMeasure.height}px`;
377+
element.style.top = `${row * this._terminal.charMeasure.height}px`;
378+
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
379+
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
380+
return element;
381+
}
321382
}
322383

323384

Diff for: src/SelectionManager.test.ts

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @license MIT
3+
*/
4+
import jsdom = require('jsdom');
5+
import { assert } from 'chai';
6+
import { ITerminal } from './Interfaces';
7+
import { CharMeasure } from './utils/CharMeasure';
8+
import { CircularList } from './utils/CircularList';
9+
import { SelectionManager } from './SelectionManager';
10+
import { SelectionModel } from './SelectionModel';
11+
12+
class TestSelectionManager extends SelectionManager {
13+
constructor(
14+
terminal: ITerminal,
15+
buffer: CircularList<any>,
16+
rowContainer: HTMLElement,
17+
charMeasure: CharMeasure
18+
) {
19+
super(terminal, buffer, rowContainer, charMeasure);
20+
}
21+
22+
public get model(): SelectionModel { return this._model; }
23+
24+
public selectLineAt(line: number): void { this._selectLineAt(line); }
25+
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
26+
27+
// Disable DOM interaction
28+
public enable(): void {}
29+
public disable(): void {}
30+
public refresh(): void {}
31+
}
32+
33+
describe('SelectionManager', () => {
34+
let window: Window;
35+
let document: Document;
36+
37+
let terminal: ITerminal;
38+
let buffer: CircularList<any>;
39+
let rowContainer: HTMLElement;
40+
let selectionManager: TestSelectionManager;
41+
42+
beforeEach(done => {
43+
jsdom.env('', (err, w) => {
44+
window = w;
45+
document = window.document;
46+
buffer = new CircularList<any>(100);
47+
terminal = <any>{ cols: 80, rows: 2 };
48+
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
49+
done();
50+
});
51+
});
52+
53+
function stringToRow(text: string): [number, string, number][] {
54+
let result: [number, string, number][] = [];
55+
for (let i = 0; i < text.length; i++) {
56+
result.push([0, text.charAt(i), 1]);
57+
}
58+
return result;
59+
}
60+
61+
describe('_selectWordAt', () => {
62+
it('should expand selection for normal width chars', () => {
63+
buffer.push(stringToRow('foo bar'));
64+
selectionManager.selectWordAt([0, 0]);
65+
assert.equal(selectionManager.selectionText, 'foo');
66+
selectionManager.selectWordAt([1, 0]);
67+
assert.equal(selectionManager.selectionText, 'foo');
68+
selectionManager.selectWordAt([2, 0]);
69+
assert.equal(selectionManager.selectionText, 'foo');
70+
selectionManager.selectWordAt([3, 0]);
71+
assert.equal(selectionManager.selectionText, ' ');
72+
selectionManager.selectWordAt([4, 0]);
73+
assert.equal(selectionManager.selectionText, 'bar');
74+
selectionManager.selectWordAt([5, 0]);
75+
assert.equal(selectionManager.selectionText, 'bar');
76+
selectionManager.selectWordAt([6, 0]);
77+
assert.equal(selectionManager.selectionText, 'bar');
78+
});
79+
it('should expand selection for whitespace', () => {
80+
buffer.push(stringToRow('a b'));
81+
selectionManager.selectWordAt([0, 0]);
82+
assert.equal(selectionManager.selectionText, 'a');
83+
selectionManager.selectWordAt([1, 0]);
84+
assert.equal(selectionManager.selectionText, ' ');
85+
selectionManager.selectWordAt([2, 0]);
86+
assert.equal(selectionManager.selectionText, ' ');
87+
selectionManager.selectWordAt([3, 0]);
88+
assert.equal(selectionManager.selectionText, ' ');
89+
selectionManager.selectWordAt([4, 0]);
90+
assert.equal(selectionManager.selectionText, 'b');
91+
});
92+
it('should expand selection for wide characters', () => {
93+
// Wide characters use a special format
94+
buffer.push([
95+
[null, '中', 2],
96+
[null, '', 0],
97+
[null, '文', 2],
98+
[null, '', 0],
99+
[null, ' ', 1],
100+
[null, 'a', 1],
101+
[null, '中', 2],
102+
[null, '', 0],
103+
[null, '文', 2],
104+
[null, '', 0],
105+
[null, 'b', 1],
106+
[null, ' ', 1],
107+
[null, 'f', 1],
108+
[null, 'o', 1],
109+
[null, 'o', 1]
110+
]);
111+
// Ensure wide characters take up 2 columns
112+
selectionManager.selectWordAt([0, 0]);
113+
assert.equal(selectionManager.selectionText, '中文');
114+
selectionManager.selectWordAt([1, 0]);
115+
assert.equal(selectionManager.selectionText, '中文');
116+
selectionManager.selectWordAt([2, 0]);
117+
assert.equal(selectionManager.selectionText, '中文');
118+
selectionManager.selectWordAt([3, 0]);
119+
assert.equal(selectionManager.selectionText, '中文');
120+
selectionManager.selectWordAt([4, 0]);
121+
assert.equal(selectionManager.selectionText, ' ');
122+
// Ensure wide characters work when wrapped in normal width characters
123+
selectionManager.selectWordAt([5, 0]);
124+
assert.equal(selectionManager.selectionText, 'a中文b');
125+
selectionManager.selectWordAt([6, 0]);
126+
assert.equal(selectionManager.selectionText, 'a中文b');
127+
selectionManager.selectWordAt([7, 0]);
128+
assert.equal(selectionManager.selectionText, 'a中文b');
129+
selectionManager.selectWordAt([8, 0]);
130+
assert.equal(selectionManager.selectionText, 'a中文b');
131+
selectionManager.selectWordAt([9, 0]);
132+
assert.equal(selectionManager.selectionText, 'a中文b');
133+
selectionManager.selectWordAt([10, 0]);
134+
assert.equal(selectionManager.selectionText, 'a中文b');
135+
selectionManager.selectWordAt([11, 0]);
136+
assert.equal(selectionManager.selectionText, ' ');
137+
// Ensure normal width characters work fine in a line containing wide characters
138+
selectionManager.selectWordAt([12, 0]);
139+
assert.equal(selectionManager.selectionText, 'foo');
140+
selectionManager.selectWordAt([13, 0]);
141+
assert.equal(selectionManager.selectionText, 'foo');
142+
selectionManager.selectWordAt([14, 0]);
143+
assert.equal(selectionManager.selectionText, 'foo');
144+
});
145+
});
146+
147+
describe('_selectLineAt', () => {
148+
it('should select the entire line', () => {
149+
buffer.push(stringToRow('foo bar'));
150+
selectionManager.selectLineAt(0);
151+
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
152+
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
153+
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
154+
});
155+
});
156+
157+
describe('selectAll', () => {
158+
it('should select the entire buffer, beyond the viewport', () => {
159+
buffer.push(stringToRow('1'));
160+
buffer.push(stringToRow('2'));
161+
buffer.push(stringToRow('3'));
162+
buffer.push(stringToRow('4'));
163+
buffer.push(stringToRow('5'));
164+
selectionManager.selectAll();
165+
terminal.ybase = buffer.length - terminal.rows;
166+
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
167+
});
168+
});
169+
});

0 commit comments

Comments
 (0)