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';
12 * The number of pixels the mouse needs to be above or below the viewport in
13 * order to scroll at the maximum speed.
15 const DRAG_SCROLL_MAX_THRESHOLD = 100;
18 * The maximum scrolling speed
20 const DRAG_SCROLL_MAX_SPEED = 5;
23 * The number of milliseconds between drag scroll updates.
25 const DRAG_SCROLL_INTERVAL = 100;
27 export class SelectionManager extends EventEmitter {
28 // TODO: Create a SelectionModel
29 private _selectionStart: [number, number];
30 private _selectionEnd: [number, number];
31 private _dragScrollAmount: number;
33 private _bufferTrimListener: any;
34 private _mouseMoveListener: EventListener;
35 private _mouseDownListener: EventListener;
36 private _mouseUpListener: EventListener;
37 private _dblClickListener: EventListener;
39 private _dragScrollTimeout: NodeJS.Timer;
42 private _terminal: ITerminal,
43 private _buffer: CircularList<any>,
44 private _rowContainer: HTMLElement,
45 private _selectionContainer: HTMLElement,
46 private _charMeasure: CharMeasure
49 this._initListeners();
53 private _initListeners() {
54 this._bufferTrimListener = (amount: number) => this._onTrim(amount);
55 this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
56 this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event);
57 this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
58 this._dblClickListener = event => this._onDblClick(<MouseEvent>event);
62 * Disables the selection manager. This is useful for when terminal mouse
66 this._selectionStart = null;
67 this._selectionEnd = null;
69 this._buffer.off('trim', this._bufferTrimListener);
70 this._rowContainer.removeEventListener('mousedown', this._mouseDownListener);
71 this._rowContainer.removeEventListener('dblclick', this._dblClickListener);
72 this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
73 this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
74 clearInterval(this._dragScrollTimeout);
78 * Enable the selection manager.
81 this._buffer.on('trim', this._bufferTrimListener);
82 this._rowContainer.addEventListener('mousedown', this._mouseDownListener);
83 this._rowContainer.addEventListener('dblclick', this._dblClickListener);
86 public get selectionText(): string {
87 if (!this._selectionStart || !this._selectionEnd) {
90 const flipValues = this._selectionStart[1] > this._selectionEnd[1] ||
91 (this._selectionStart[1] === this._selectionEnd[1] && this._selectionStart[0] > this._selectionEnd[0]);
92 const start = flipValues ? this._selectionEnd : this._selectionStart;
93 const end = flipValues ? this._selectionStart : this._selectionEnd;
94 const startRowEndCol = start[1] === end[1] ? end[0] : null;
95 let result: string[] = [];
96 result.push(this._translateBufferLineToString(this._buffer.get(start[1]), start[0], startRowEndCol));
97 for (let i = start[1] + 1; i <= end[1] - 1; i++) {
98 result.push(this._translateBufferLineToString(this._buffer.get(i)));
100 if (start[1] !== end[1]) {
101 result.push(this._translateBufferLineToString(this._buffer.get(end[1]), 0, end[1]));
103 console.log('selectionText result: ' + result);
104 return result.join('\n');
107 private _translateBufferLineToString(line: any, startCol: number = 0, endCol: number = null): string {
108 // TODO: This function should live in a buffer or buffer line class
109 endCol = endCol || line.length;
111 for (let i = startCol; i < endCol; i++) {
112 result += line[i][1];
114 // TODO: Trim line here instead of in handlers/Clipboard?
115 // TODO: Only trim off the whitespace at the end of a line
116 // TODO: Handle the double-width character case
121 * Redraws the selection.
123 public refresh(): void {
124 // TODO: Figure out when to refresh the selection vs when to refresh the viewport
125 this.emit('refresh', { start: this._selectionStart, end: this._selectionEnd });
129 * Handle the buffer being trimmed, adjust the selection position.
130 * @param amount The amount the buffer is being trimmed.
132 private _onTrim(amount: number) {
133 // Adjust the selection position based on the trimmed amount.
134 if (this._selectionStart) {
135 this._selectionStart[0] -= amount;
137 if (this._selectionEnd) {
138 this._selectionEnd[0] -= amount;
141 // The selection has moved off the buffer, clear it.
142 if (this._selectionEnd && this._selectionEnd[0] < 0) {
143 this._selectionStart = null;
144 this._selectionEnd = null;
149 // If the selection start is trimmed, ensure the start column is 0.
150 if (this._selectionStart && this._selectionStart[0] < 0) {
151 this._selectionStart[1] = 0;
155 // TODO: Handle splice/shiftElements in the buffer (just clear the selection?)
157 private _getMouseBufferCoords(event: MouseEvent): [number, number] {
158 const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows);
160 // Convert to 0-based
163 // Convert viewport coords to buffer coords
164 coords[1] += this._terminal.ydisp;
168 private _getMouseEventScrollAmount(event: MouseEvent): number {
169 let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
170 const terminalHeight = this._terminal.rows * this._charMeasure.height;
171 if (offset >= 0 && offset <= terminalHeight) {
174 if (offset > terminalHeight) {
175 offset -= terminalHeight;
178 offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
179 offset /= DRAG_SCROLL_MAX_THRESHOLD;
180 return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
184 * Handles te mousedown event, setting up for a new selection.
185 * @param event The mousedown event.
187 private _onMouseDown(event: MouseEvent) {
188 // TODO: On right click move the text into the textbox so it can be copied via the context menu
190 // Only action the primary button
191 if (event.button !== 0) {
195 this._selectionStart = this._getMouseBufferCoords(event);
196 if (this._selectionStart) {
197 this._selectionEnd = null;
198 // Listen on the document so that dragging outside of viewport works
199 this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
200 this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
201 this._dragScrollTimeout = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
207 * Handles the mousemove event when the mouse button is down, recording the
208 * end of the selection and refreshing the selection.
209 * @param event The mousemove event.
211 private _onMouseMove(event: MouseEvent) {
212 this._selectionEnd = this._getMouseBufferCoords(event);
213 // TODO: Perhaps the actual selection setting could be merged into _dragScroll?
214 this._dragScrollAmount = this._getMouseEventScrollAmount(event);
215 // If the cursor was above or below the viewport, make sure it's at the
216 // start or end of the viewport respectively
217 if (this._dragScrollAmount > 0) {
218 this._selectionEnd[0] = this._terminal.cols - 1;
219 } else if (this._dragScrollAmount < 0) {
220 this._selectionEnd[0] = 0;
222 // TODO: Only draw here if the selection changes
226 private _dragScroll() {
227 if (this._dragScrollAmount) {
228 this._terminal.scrollDisp(this._dragScrollAmount, false);
229 // Re-evaluate selection
230 if (this._dragScrollAmount > 0) {
231 this._selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows];
233 this._selectionEnd = [0, this._terminal.ydisp];
240 * Handles the mouseup event, removing the mousemove listener when
242 * @param event The mouseup event.
244 private _onMouseUp(event: MouseEvent) {
245 this._dragScrollAmount = 0;
246 if (!this._selectionStart) {
249 this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
250 this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
253 private _onDblClick(event: MouseEvent) {
254 const coords = this._getMouseBufferCoords(event);
256 this._selectWordAt(coords);
261 * Selects the word at the coordinates specified. Words are defined as all
262 * non-whitespace characters.
263 * @param coords The coordinates to get the word at.
265 private _selectWordAt(coords: [number, number]): void {
266 // TODO: Handle double click and drag in both directions!
268 const line = this._translateBufferLineToString(this._buffer.get(coords[1]));
269 // Expand the string in both directions until a space is hit
270 let startCol = coords[0];
271 let endCol = coords[0];
272 while (startCol > 0 && line.charAt(startCol - 1) !== ' ') {
275 while (endCol < line.length && line.charAt(endCol) !== ' ') {
278 this._selectionStart = [startCol, coords[1]];
279 this._selectionEnd = [endCol, coords[1]];