5 import { CharMeasure } from './utils/CharMeasure';
6 import { CircularList } from './utils/CircularList';
7 import { EventEmitter } from './EventEmitter';
8 import * as Mouse from './utils/Mouse';
9 import { ITerminal } from './Interfaces';
11 export class SelectionManager extends EventEmitter {
12 // TODO: Create a SelectionModel
13 private _selectionStart: [number, number];
14 private _selectionEnd: [number, number];
16 private _bufferTrimListener: any;
17 private _mouseMoveListener: EventListener;
18 private _mouseDownListener: EventListener;
19 private _mouseUpListener: EventListener;
20 private _dblClickListener: EventListener;
23 private _terminal: ITerminal,
24 private _buffer: CircularList<any>,
25 private _rowContainer: HTMLElement,
26 private _selectionContainer: HTMLElement,
27 private _charMeasure: CharMeasure
30 this._initListeners();
34 private _initListeners() {
35 this._bufferTrimListener = (amount: number) => this._onTrim(amount);
36 this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
37 this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event);
38 this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
39 this._dblClickListener = event => this._onDblClick(<MouseEvent>event);
43 * Disables the selection manager. This is useful for when terminal mouse
47 this._selectionStart = null;
48 this._selectionEnd = null;
50 this._buffer.off('trim', this._bufferTrimListener);
51 this._rowContainer.removeEventListener('mousedown', this._mouseDownListener);
52 this._rowContainer.removeEventListener('mouseup', this._mouseUpListener);
53 this._rowContainer.removeEventListener('dblclick', this._dblClickListener);
54 this._rowContainer.removeEventListener('mousemove', this._mouseMoveListener);
58 * Enable the selection manager.
61 this._buffer.on('trim', this._bufferTrimListener);
62 this._rowContainer.addEventListener('mousedown', this._mouseDownListener);
63 this._rowContainer.addEventListener('mouseup', this._mouseUpListener);
64 this._rowContainer.addEventListener('dblclick', this._dblClickListener);
67 public get selectionText(): string {
68 if (!this._selectionStart || !this._selectionEnd) {
71 const flipValues = this._selectionStart[1] > this._selectionEnd[1] ||
72 (this._selectionStart[1] === this._selectionEnd[1] && this._selectionStart[0] > this._selectionEnd[0]);
73 const start = flipValues ? this._selectionEnd : this._selectionStart;
74 const end = flipValues ? this._selectionStart : this._selectionEnd;
75 const startRowEndCol = start[1] === end[1] ? end[0] : null;
76 let result: string[] = [];
77 result.push(this._translateBufferLineToString(this._buffer.get(start[1]), start[0], startRowEndCol));
78 for (let i = start[1] + 1; i <= end[1] - 1; i++) {
79 result.push(this._translateBufferLineToString(this._buffer.get(i)));
81 if (start[1] !== end[1]) {
82 result.push(this._translateBufferLineToString(this._buffer.get(end[1]), 0, end[1]));
84 console.log('selectionText result: ' + result);
85 return result.join('\n');
88 private _translateBufferLineToString(line: any, startCol: number = 0, endCol: number = null): string {
89 // TODO: This function should live in a buffer or buffer line class
90 endCol = endCol || line.length
92 for (let i = startCol; i < endCol; i++) {
95 // TODO: Trim line here instead of in handlers/Clipboard?
96 // TODO: Only trim off the whitespace at the end of a line
97 // TODO: Handle the double-width character case
102 * Redraws the selection.
104 public refresh(): void {
105 // TODO: Figure out when to refresh the selection vs when to refresh the viewport
106 this.emit('refresh', { start: this._selectionStart, end: this._selectionEnd });
110 * Handle the buffer being trimmed, adjust the selection position.
111 * @param amount The amount the buffer is being trimmed.
113 private _onTrim(amount: number) {
114 // Adjust the selection position based on the trimmed amount.
115 this._selectionStart[0] -= amount;
116 this._selectionEnd[0] -= amount;
118 // The selection has moved off the buffer, clear it.
119 if (this._selectionEnd[0] < 0) {
120 this._selectionStart = null;
121 this._selectionEnd = null;
126 // If the selection start is trimmed, ensure the start column is 0.
127 if (this._selectionStart[0] < 0) {
128 this._selectionStart[1] = 0;
132 // TODO: Handle splice/shiftElements in the buffer (just clear the selection?)
134 private _getMouseBufferCoords(event: MouseEvent) {
135 const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure);
136 // Convert to 0-based
139 // Convert viewport coords to buffer coords
140 coords[1] += this._terminal.ydisp;
145 * Handles te mousedown event, setting up for a new selection.
146 * @param event The mousedown event.
148 private _onMouseDown(event: MouseEvent) {
149 this._selectionStart = this._getMouseBufferCoords(event);
150 if (this._selectionStart) {
151 this._selectionEnd = null;
152 this._rowContainer.addEventListener('mousemove', this._mouseMoveListener);
158 * Handles the mousemove event when the mouse button is down, recording the
159 * end of the selection and refreshing the selection.
160 * @param event The mousemove event.
162 private _onMouseMove(event: MouseEvent) {
163 this._selectionEnd = this._getMouseBufferCoords(event);
164 // TODO: Only draw here if the selection changes
169 * Handles the mouseup event, removing the mousemove listener when
171 * @param event The mouseup event.
173 private _onMouseUp(event: MouseEvent) {
174 if (!this._selectionStart) {
177 this._rowContainer.removeEventListener('mousemove', this._mouseMoveListener);
180 private _onDblClick(event: MouseEvent) {
181 const coords = this._getMouseBufferCoords(event);
183 this._selectWordAt(coords);
188 * Selects the word at the coordinates specified. Words are defined as all
189 * non-whitespace characters.
190 * @param coords The coordinates to get the word at.
192 private _selectWordAt(coords: [number, number]): void {
193 // TODO: Handle double click and drag in both directions!
195 const line = this._translateBufferLineToString(this._buffer.get(coords[1]));
196 // Expand the string in both directions until a space is hit
197 let startCol = coords[0];
198 let endCol = coords[0];
199 while (startCol > 0 && line.charAt(startCol - 1) !== ' ') {
202 while (endCol < line.length && line.charAt(endCol) !== ' ') {
205 this._selectionStart = [startCol, coords[1]];
206 this._selectionEnd = [endCol, coords[1]];