]> git.proxmox.com Git - mirror_xterm.js.git/blame - src/SelectionManager.ts
Only trim the right whitespace from selection
[mirror_xterm.js.git] / src / SelectionManager.ts
CommitLineData
70fda994
DI
1/**
2 * @license MIT
3 */
4
5import { CharMeasure } from './utils/CharMeasure';
6import { CircularList } from './utils/CircularList';
b594407c 7import { EventEmitter } from './EventEmitter';
70fda994 8import * as Mouse from './utils/Mouse';
ad3ae67e 9import { ITerminal } from './Interfaces';
f7d6ab5f 10import { SelectionModel } from './SelectionModel';
70fda994 11
0dc3dd03
DI
12/**
13 * The number of pixels the mouse needs to be above or below the viewport in
14 * order to scroll at the maximum speed.
15 */
16const DRAG_SCROLL_MAX_THRESHOLD = 100;
17
18/**
19 * The maximum scrolling speed
20 */
21const DRAG_SCROLL_MAX_SPEED = 5;
22
23/**
24 * The number of milliseconds between drag scroll updates.
25 */
26const DRAG_SCROLL_INTERVAL = 100;
27
9f271de8
DI
28/**
29 * The amount of time before mousedown events are no stacked to create double
30 * click events.
31 */
32const CLEAR_MOUSE_DOWN_TIME = 400;
33
b594407c 34export class SelectionManager extends EventEmitter {
f7d6ab5f 35 private _model: SelectionModel;
9f271de8
DI
36
37 /**
38 * The amount to scroll every drag scroll update (depends on how far the mouse
39 * drag is above or below the terminal).
40 */
0dc3dd03 41 private _dragScrollAmount: number;
70fda994 42
9f271de8
DI
43 /**
44 * The last time the mousedown event fired, this is used to track double and
45 * triple clicks.
46 */
47 private _lastMouseDownTime: number;
48
49 private _clickCount: number;
50
ab40908f 51 private _bufferTrimListener: any;
70fda994 52 private _mouseMoveListener: EventListener;
ab40908f
DI
53 private _mouseDownListener: EventListener;
54 private _mouseUpListener: EventListener;
70fda994 55
0dc3dd03
DI
56 private _dragScrollTimeout: NodeJS.Timer;
57
ad3ae67e
DI
58 constructor(
59 private _terminal: ITerminal,
60 private _buffer: CircularList<any>,
61 private _rowContainer: HTMLElement,
62 private _selectionContainer: HTMLElement,
63 private _charMeasure: CharMeasure
64 ) {
b594407c 65 super();
ab40908f
DI
66 this._initListeners();
67 this.enable();
9f271de8 68
f7d6ab5f 69 this._model = new SelectionModel(_terminal);
9f271de8 70 this._lastMouseDownTime = 0;
70fda994
DI
71 }
72
ab40908f
DI
73 private _initListeners() {
74 this._bufferTrimListener = (amount: number) => this._onTrim(amount);
70fda994 75 this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
ab40908f
DI
76 this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event);
77 this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
ab40908f 78 }
70fda994 79
ab40908f
DI
80 /**
81 * Disables the selection manager. This is useful for when terminal mouse
82 * are enabled.
83 */
84 public disable() {
f7d6ab5f
DI
85 this._model.selectionStart = null;
86 this._model.selectionEnd = null;
ab40908f
DI
87 this.refresh();
88 this._buffer.off('trim', this._bufferTrimListener);
89 this._rowContainer.removeEventListener('mousedown', this._mouseDownListener);
0dc3dd03
DI
90 this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
91 this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
92 clearInterval(this._dragScrollTimeout);
ab40908f
DI
93 }
94
95 /**
96 * Enable the selection manager.
97 */
98 public enable() {
99 this._buffer.on('trim', this._bufferTrimListener);
100 this._rowContainer.addEventListener('mousedown', this._mouseDownListener);
70fda994
DI
101 }
102
9f271de8
DI
103 /**
104 * Gets the text currently selected.
105 */
70fda994 106 public get selectionText(): string {
f7d6ab5f
DI
107 const start = this._model.finalSelectionStart;
108 const end = this._model.finalSelectionEnd;
e29ab294 109 if (!start || !end) {
293ae18a 110 return '';
70fda994 111 }
43c796a7 112
43c796a7 113 // Get first row
293ae18a 114 const startRowEndCol = start[1] === end[1] ? end[0] : null;
32b34cbe 115 let result: string[] = [];
ec61f3ac 116 result.push(this._translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
43c796a7
DI
117
118 // Get middle rows
293ae18a 119 for (let i = start[1] + 1; i <= end[1] - 1; i++) {
ec61f3ac 120 result.push(this._translateBufferLineToString(this._buffer.get(i), true));
32b34cbe 121 }
43c796a7
DI
122
123 // Get final row
293ae18a 124 if (start[1] !== end[1]) {
ec61f3ac 125 result.push(this._translateBufferLineToString(this._buffer.get(end[1]), true, 0, end[0]));
32b34cbe 126 }
597c6939 127 console.log('selectionText result: ' + result);
32b34cbe
DI
128 return result.join('\n');
129 }
130
ec61f3ac 131 private _translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
32b34cbe 132 // TODO: This function should live in a buffer or buffer line class
32b34cbe 133 // TODO: Handle the double-width character case
ec61f3ac
DI
134
135 // Get full line
136 let lineString = '';
137 for (let i = 0; i < line.length; i++) {
138 lineString += line[i][1];
139 }
140
141 let finalEndCol = endCol || line.length
142
143 if (trimRight) {
144 const rightWhitespaceIndex = lineString.search(/\s+$/);
145 finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
146 // Return the empty string if only trimmed whitespace is selected
147 if (finalEndCol <= startCol) {
148 return '';
149 }
150 }
151
152 return lineString.substring(startCol, finalEndCol);
70fda994
DI
153 }
154
207c4cf9
DI
155 /**
156 * Redraws the selection.
157 */
158 public refresh(): void {
b594407c 159 // TODO: Figure out when to refresh the selection vs when to refresh the viewport
f7d6ab5f 160 this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
25152e44
DI
161 }
162
163 /**
164 * Selects all text within the terminal.
165 */
166 public selectAll(): void {
f7d6ab5f 167 this._model.isSelectAllActive = true;
25152e44 168 this.refresh();
207c4cf9
DI
169 }
170
171 /**
172 * Handle the buffer being trimmed, adjust the selection position.
173 * @param amount The amount the buffer is being trimmed.
174 */
70fda994 175 private _onTrim(amount: number) {
f7d6ab5f
DI
176 const needsRefresh = this._model.onTrim(amount);
177 if (needsRefresh) {
207c4cf9 178 this.refresh();
207c4cf9 179 }
70fda994
DI
180 }
181
32b34cbe
DI
182 // TODO: Handle splice/shiftElements in the buffer (just clear the selection?)
183
0dc3dd03
DI
184 private _getMouseBufferCoords(event: MouseEvent): [number, number] {
185 const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows);
186 console.log(coords);
ad3ae67e
DI
187 // Convert to 0-based
188 coords[0]--;
189 coords[1]--;
190 // Convert viewport coords to buffer coords
191 coords[1] += this._terminal.ydisp;
192 return coords;
b36d8780
DI
193 }
194
0dc3dd03
DI
195 private _getMouseEventScrollAmount(event: MouseEvent): number {
196 let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
197 const terminalHeight = this._terminal.rows * this._charMeasure.height;
198 if (offset >= 0 && offset <= terminalHeight) {
199 return 0;
200 }
201 if (offset > terminalHeight) {
202 offset -= terminalHeight;
203 }
204
205 offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
206 offset /= DRAG_SCROLL_MAX_THRESHOLD;
207 return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
208 }
209
e63fdf58
DI
210 /**
211 * Handles te mousedown event, setting up for a new selection.
212 * @param event The mousedown event.
213 */
70fda994 214 private _onMouseDown(event: MouseEvent) {
0dc3dd03
DI
215 // Only action the primary button
216 if (event.button !== 0) {
217 return;
218 }
219
9f271de8
DI
220 this._setMouseClickCount();
221 console.log(this._clickCount);
222
223 if (this._clickCount === 1) {
224 this._onSingleClick(event);
225 } else if (this._clickCount === 2) {
226 this._onDoubleClick(event);
227 } else if (this._clickCount === 3) {
228 this._onTripleClick(event);
229 }
e29ab294
DI
230
231 // Listen on the document so that dragging outside of viewport works
232 this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
233 this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
234 this._dragScrollTimeout = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
235 this.refresh();
9f271de8
DI
236 }
237
238 private _onSingleClick(event: MouseEvent): void {
f7d6ab5f
DI
239 this._model.selectionStartLength = 0;
240 this._model.isSelectAllActive = false;
241 this._model.selectionStart = this._getMouseBufferCoords(event);
242 if (this._model.selectionStart) {
243 this._model.selectionEnd = null;
70fda994
DI
244 }
245 }
246
9f271de8
DI
247 private _onDoubleClick(event: MouseEvent): void {
248 const coords = this._getMouseBufferCoords(event);
249 if (coords) {
250 this._selectWordAt(coords);
251 }
252 }
253
254 private _onTripleClick(event: MouseEvent): void {
255 const coords = this._getMouseBufferCoords(event);
256 if (coords) {
257 this._selectLineAt(coords[1]);
258 }
259 }
260
261 private _setMouseClickCount(): void {
262 let currentTime = (new Date()).getTime();
263 if (currentTime - this._lastMouseDownTime > CLEAR_MOUSE_DOWN_TIME) {
264 this._clickCount = 0;
265 }
266 this._lastMouseDownTime = currentTime;
267 this._clickCount++;
268
269 // TODO: Invalidate click count if the position is different
270 }
271
e63fdf58
DI
272 /**
273 * Handles the mousemove event when the mouse button is down, recording the
274 * end of the selection and refreshing the selection.
275 * @param event The mousemove event.
276 */
70fda994 277 private _onMouseMove(event: MouseEvent) {
f7d6ab5f 278 this._model.selectionEnd = this._getMouseBufferCoords(event);
0dc3dd03
DI
279 // TODO: Perhaps the actual selection setting could be merged into _dragScroll?
280 this._dragScrollAmount = this._getMouseEventScrollAmount(event);
281 // If the cursor was above or below the viewport, make sure it's at the
282 // start or end of the viewport respectively
283 if (this._dragScrollAmount > 0) {
f7d6ab5f 284 this._model.selectionEnd[0] = this._terminal.cols - 1;
0dc3dd03 285 } else if (this._dragScrollAmount < 0) {
f7d6ab5f 286 this._model.selectionEnd[0] = 0;
0dc3dd03 287 }
207c4cf9
DI
288 // TODO: Only draw here if the selection changes
289 this.refresh();
f7d6ab5f
DI
290 console.log('start: ', this._model.selectionStart);
291 console.log('start final: ', this._model.finalSelectionStart);
292 console.log('end: ', this._model.selectionEnd);
293 console.log('end final: ', this._model.finalSelectionEnd);
70fda994
DI
294 }
295
0dc3dd03
DI
296 private _dragScroll() {
297 if (this._dragScrollAmount) {
298 this._terminal.scrollDisp(this._dragScrollAmount, false);
299 // Re-evaluate selection
300 if (this._dragScrollAmount > 0) {
f7d6ab5f 301 this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows];
0dc3dd03 302 } else {
f7d6ab5f 303 this._model.selectionEnd = [0, this._terminal.ydisp];
0dc3dd03
DI
304 }
305 this.refresh();
306 }
307 }
308
e63fdf58
DI
309 /**
310 * Handles the mouseup event, removing the mousemove listener when
311 * appropriate.
312 * @param event The mouseup event.
313 */
70fda994 314 private _onMouseUp(event: MouseEvent) {
b3b2bd1f 315 this._dragScrollAmount = 0;
f7d6ab5f 316 if (!this._model.selectionStart) {
70fda994
DI
317 return;
318 }
0dc3dd03
DI
319 this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
320 this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
70fda994 321 }
597c6939 322
597c6939
DI
323 /**
324 * Selects the word at the coordinates specified. Words are defined as all
325 * non-whitespace characters.
326 * @param coords The coordinates to get the word at.
327 */
328 private _selectWordAt(coords: [number, number]): void {
ec61f3ac 329 const line = this._translateBufferLineToString(this._buffer.get(coords[1]), false);
597c6939
DI
330 // Expand the string in both directions until a space is hit
331 let startCol = coords[0];
332 let endCol = coords[0];
333 while (startCol > 0 && line.charAt(startCol - 1) !== ' ') {
334 startCol--;
335 }
336 while (endCol < line.length && line.charAt(endCol) !== ' ') {
337 endCol++;
338 }
f7d6ab5f
DI
339 this._model.selectionStart = [startCol, coords[1]];
340 this._model.selectionStartLength = endCol - startCol;
597c6939 341 }
9f271de8
DI
342
343 private _selectLineAt(line: number): void {
f7d6ab5f
DI
344 this._model.selectionStart = [0, line];
345 this._model.selectionStartLength = this._terminal.cols;
9f271de8 346 }
70fda994 347}