]>
Commit | Line | Data |
---|---|---|
70fda994 DI |
1 | /** |
2 | * @license MIT | |
3 | */ | |
4 | ||
5 | import { CharMeasure } from './utils/CharMeasure'; | |
6 | import { CircularList } from './utils/CircularList'; | |
b594407c | 7 | import { EventEmitter } from './EventEmitter'; |
70fda994 | 8 | import * as Mouse from './utils/Mouse'; |
ad3ae67e | 9 | import { ITerminal } from './Interfaces'; |
f7d6ab5f | 10 | import { 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 | */ | |
b8129910 | 16 | const DRAG_SCROLL_MAX_THRESHOLD = 50; |
0dc3dd03 DI |
17 | |
18 | /** | |
19 | * The maximum scrolling speed | |
20 | */ | |
b8129910 | 21 | const DRAG_SCROLL_MAX_SPEED = 15; |
0dc3dd03 DI |
22 | |
23 | /** | |
24 | * The number of milliseconds between drag scroll updates. | |
25 | */ | |
b8129910 | 26 | const DRAG_SCROLL_INTERVAL = 50; |
0dc3dd03 | 27 | |
9f271de8 | 28 | /** |
f380153f DI |
29 | * The amount of time before mousedown events are no longer stacked to create |
30 | * double/triple click events. | |
9f271de8 DI |
31 | */ |
32 | const CLEAR_MOUSE_DOWN_TIME = 400; | |
33 | ||
f380153f DI |
34 | /** |
35 | * The number of pixels in each direction that the mouse must move before | |
36 | * mousedown events are no longer stacked to create double/triple click events. | |
37 | */ | |
38 | const CLEAR_MOUSE_DISTANCE = 10; | |
39 | ||
2621be81 DI |
40 | // TODO: Move these constants elsewhere, they belong in a buffer or buffer |
41 | // data/line class. | |
54e7f65d DI |
42 | const LINE_DATA_CHAR_INDEX = 1; |
43 | const LINE_DATA_WIDTH_INDEX = 2; | |
44 | ||
b594407c | 45 | export class SelectionManager extends EventEmitter { |
fd91c5e1 | 46 | protected _model: SelectionModel; |
9f271de8 DI |
47 | |
48 | /** | |
49 | * The amount to scroll every drag scroll update (depends on how far the mouse | |
50 | * drag is above or below the terminal). | |
51 | */ | |
0dc3dd03 | 52 | private _dragScrollAmount: number; |
70fda994 | 53 | |
9f271de8 DI |
54 | /** |
55 | * The last time the mousedown event fired, this is used to track double and | |
56 | * triple clicks. | |
57 | */ | |
58 | private _lastMouseDownTime: number; | |
59 | ||
f380153f DI |
60 | /** |
61 | * The last position the mouse was clicked [x, y]. | |
62 | */ | |
63 | private _lastMousePosition: [number, number]; | |
64 | ||
2621be81 DI |
65 | /** |
66 | * The number of clicks of the mousedown event. This is used to keep track of | |
67 | * double and triple clicks. | |
68 | */ | |
9f271de8 DI |
69 | private _clickCount: number; |
70 | ||
2621be81 DI |
71 | /** |
72 | * Whether line select mode is active, this occurs after a triple click. | |
73 | */ | |
5bc11121 DI |
74 | private _isLineSelectModeActive: boolean; |
75 | ||
d0b603d0 DI |
76 | /** |
77 | * A setInterval timer that is active while the mouse is down whose callback | |
78 | * scrolls the viewport when necessary. | |
79 | */ | |
80 | private _dragScrollIntervalTimer: NodeJS.Timer; | |
81 | ||
13c401cb DI |
82 | private _refreshAnimationFrame: number; |
83 | ||
ab40908f | 84 | private _bufferTrimListener: any; |
70fda994 | 85 | private _mouseMoveListener: EventListener; |
ab40908f DI |
86 | private _mouseDownListener: EventListener; |
87 | private _mouseUpListener: EventListener; | |
70fda994 | 88 | |
ad3ae67e DI |
89 | constructor( |
90 | private _terminal: ITerminal, | |
91 | private _buffer: CircularList<any>, | |
92 | private _rowContainer: HTMLElement, | |
ad3ae67e DI |
93 | private _charMeasure: CharMeasure |
94 | ) { | |
b594407c | 95 | super(); |
ab40908f DI |
96 | this._initListeners(); |
97 | this.enable(); | |
9f271de8 | 98 | |
f7d6ab5f | 99 | this._model = new SelectionModel(_terminal); |
9f271de8 | 100 | this._lastMouseDownTime = 0; |
5bc11121 | 101 | this._isLineSelectModeActive = false; |
70fda994 DI |
102 | } |
103 | ||
d0b603d0 DI |
104 | /** |
105 | * Initializes listener variables. | |
106 | */ | |
ab40908f DI |
107 | private _initListeners() { |
108 | this._bufferTrimListener = (amount: number) => this._onTrim(amount); | |
70fda994 | 109 | this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event); |
ab40908f DI |
110 | this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event); |
111 | this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event); | |
ab40908f | 112 | } |
70fda994 | 113 | |
ab40908f DI |
114 | /** |
115 | * Disables the selection manager. This is useful for when terminal mouse | |
116 | * are enabled. | |
117 | */ | |
118 | public disable() { | |
f7d6ab5f DI |
119 | this._model.selectionStart = null; |
120 | this._model.selectionEnd = null; | |
ab40908f DI |
121 | this.refresh(); |
122 | this._buffer.off('trim', this._bufferTrimListener); | |
123 | this._rowContainer.removeEventListener('mousedown', this._mouseDownListener); | |
b8129910 | 124 | this._removeMouseDownListeners(); |
ab40908f DI |
125 | } |
126 | ||
127 | /** | |
128 | * Enable the selection manager. | |
129 | */ | |
130 | public enable() { | |
f380153f DI |
131 | // Only adjust the selection on trim, shiftElements is rarely used (only in |
132 | // reverseIndex) and delete in a splice is only ever used when the same | |
133 | // number of elements was just added. Given this is could actually be | |
134 | // beneficial to leave the selection as is for these cases. | |
ab40908f DI |
135 | this._buffer.on('trim', this._bufferTrimListener); |
136 | this._rowContainer.addEventListener('mousedown', this._mouseDownListener); | |
70fda994 DI |
137 | } |
138 | ||
9f271de8 DI |
139 | /** |
140 | * Gets the text currently selected. | |
141 | */ | |
70fda994 | 142 | public get selectionText(): string { |
f7d6ab5f DI |
143 | const start = this._model.finalSelectionStart; |
144 | const end = this._model.finalSelectionEnd; | |
e29ab294 | 145 | if (!start || !end) { |
293ae18a | 146 | return ''; |
70fda994 | 147 | } |
43c796a7 | 148 | |
43c796a7 | 149 | // Get first row |
293ae18a | 150 | const startRowEndCol = start[1] === end[1] ? end[0] : null; |
32b34cbe | 151 | let result: string[] = []; |
ec61f3ac | 152 | result.push(this._translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol)); |
43c796a7 DI |
153 | |
154 | // Get middle rows | |
293ae18a | 155 | for (let i = start[1] + 1; i <= end[1] - 1; i++) { |
ec61f3ac | 156 | result.push(this._translateBufferLineToString(this._buffer.get(i), true)); |
32b34cbe | 157 | } |
43c796a7 DI |
158 | |
159 | // Get final row | |
293ae18a | 160 | if (start[1] !== end[1]) { |
ec61f3ac | 161 | result.push(this._translateBufferLineToString(this._buffer.get(end[1]), true, 0, end[0])); |
32b34cbe | 162 | } |
2b243182 | 163 | |
32b34cbe DI |
164 | return result.join('\n'); |
165 | } | |
166 | ||
d0b603d0 DI |
167 | /** |
168 | * Translates a buffer line to a string, with optional start and end columns. | |
169 | * Wide characters will count as two columns in the resulting string. This | |
170 | * function is useful for getting the actual text underneath the raw selection | |
171 | * position. | |
172 | * @param line The line being translated. | |
173 | * @param trimRight Whether to trim whitespace to the right. | |
174 | * @param startCol The column to start at. | |
175 | * @param endCol The column to end at. | |
176 | */ | |
ec61f3ac | 177 | private _translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string { |
32b34cbe | 178 | // TODO: This function should live in a buffer or buffer line class |
ec61f3ac DI |
179 | |
180 | // Get full line | |
181 | let lineString = ''; | |
54e7f65d DI |
182 | let widthAdjustedStartCol = startCol; |
183 | let widthAdjustedEndCol = endCol; | |
ec61f3ac | 184 | for (let i = 0; i < line.length; i++) { |
54e7f65d DI |
185 | const char = line[i]; |
186 | lineString += char[LINE_DATA_CHAR_INDEX]; | |
187 | // Adjust start and end cols for wide characters if they affect their | |
188 | // column indexes | |
189 | if (char[LINE_DATA_WIDTH_INDEX] === 0) { | |
190 | if (startCol >= i) { | |
191 | widthAdjustedStartCol--; | |
192 | } | |
193 | if (endCol >= i) { | |
194 | widthAdjustedEndCol--; | |
195 | } | |
196 | } | |
ec61f3ac DI |
197 | } |
198 | ||
54e7f65d DI |
199 | // Calculate the final end col by trimming whitespace on the right of the |
200 | // line if needed. | |
d3865ad2 | 201 | let finalEndCol = widthAdjustedEndCol || line.length; |
ec61f3ac DI |
202 | if (trimRight) { |
203 | const rightWhitespaceIndex = lineString.search(/\s+$/); | |
a53a0acd DI |
204 | if (rightWhitespaceIndex !== -1) { |
205 | finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); | |
206 | } | |
ec61f3ac | 207 | // Return the empty string if only trimmed whitespace is selected |
54e7f65d | 208 | if (finalEndCol <= widthAdjustedStartCol) { |
ec61f3ac DI |
209 | return ''; |
210 | } | |
211 | } | |
212 | ||
54e7f65d | 213 | return lineString.substring(widthAdjustedStartCol, finalEndCol); |
70fda994 DI |
214 | } |
215 | ||
207c4cf9 | 216 | /** |
13c401cb | 217 | * Queues a refresh, redrawing the selection on the next opportunity. |
207c4cf9 DI |
218 | */ |
219 | public refresh(): void { | |
13c401cb DI |
220 | if (!this._refreshAnimationFrame) { |
221 | this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); | |
222 | } | |
223 | } | |
224 | ||
225 | /** | |
226 | * Fires the refresh event, causing consumers to pick it up and redraw the | |
227 | * selection state. | |
228 | */ | |
229 | private _refresh(): void { | |
230 | this._refreshAnimationFrame = null; | |
f7d6ab5f | 231 | this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd }); |
25152e44 DI |
232 | } |
233 | ||
234 | /** | |
235 | * Selects all text within the terminal. | |
236 | */ | |
237 | public selectAll(): void { | |
f7d6ab5f | 238 | this._model.isSelectAllActive = true; |
25152e44 | 239 | this.refresh(); |
207c4cf9 DI |
240 | } |
241 | ||
242 | /** | |
243 | * Handle the buffer being trimmed, adjust the selection position. | |
244 | * @param amount The amount the buffer is being trimmed. | |
245 | */ | |
70fda994 | 246 | private _onTrim(amount: number) { |
f7d6ab5f DI |
247 | const needsRefresh = this._model.onTrim(amount); |
248 | if (needsRefresh) { | |
207c4cf9 | 249 | this.refresh(); |
207c4cf9 | 250 | } |
70fda994 DI |
251 | } |
252 | ||
d0b603d0 DI |
253 | /** |
254 | * Gets the 0-based [x, y] buffer coordinates of the current mouse event. | |
255 | * @param event The mouse event. | |
256 | */ | |
0dc3dd03 DI |
257 | private _getMouseBufferCoords(event: MouseEvent): [number, number] { |
258 | const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows); | |
ad3ae67e DI |
259 | // Convert to 0-based |
260 | coords[0]--; | |
261 | coords[1]--; | |
262 | // Convert viewport coords to buffer coords | |
263 | coords[1] += this._terminal.ydisp; | |
264 | return coords; | |
b36d8780 DI |
265 | } |
266 | ||
d0b603d0 DI |
267 | /** |
268 | * Gets the amount the viewport should be scrolled based on how far out of the | |
269 | * terminal the mouse is. | |
270 | * @param event The mouse event. | |
271 | */ | |
0dc3dd03 DI |
272 | private _getMouseEventScrollAmount(event: MouseEvent): number { |
273 | let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1]; | |
274 | const terminalHeight = this._terminal.rows * this._charMeasure.height; | |
275 | if (offset >= 0 && offset <= terminalHeight) { | |
276 | return 0; | |
277 | } | |
278 | if (offset > terminalHeight) { | |
279 | offset -= terminalHeight; | |
280 | } | |
281 | ||
282 | offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD); | |
283 | offset /= DRAG_SCROLL_MAX_THRESHOLD; | |
284 | return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1)); | |
285 | } | |
286 | ||
e63fdf58 DI |
287 | /** |
288 | * Handles te mousedown event, setting up for a new selection. | |
289 | * @param event The mousedown event. | |
290 | */ | |
70fda994 | 291 | private _onMouseDown(event: MouseEvent) { |
0dc3dd03 DI |
292 | // Only action the primary button |
293 | if (event.button !== 0) { | |
294 | return; | |
295 | } | |
296 | ||
b8129910 DI |
297 | // Reset drag scroll state |
298 | this._dragScrollAmount = 0; | |
299 | ||
f380153f | 300 | this._setMouseClickCount(event); |
9f271de8 | 301 | |
db8ded2a DI |
302 | if (event.shiftKey) { |
303 | this._onShiftClick(event); | |
304 | } else { | |
305 | if (this._clickCount === 1) { | |
306 | this._onSingleClick(event); | |
307 | } else if (this._clickCount === 2) { | |
308 | this._onDoubleClick(event); | |
309 | } else if (this._clickCount === 3) { | |
310 | this._onTripleClick(event); | |
311 | } | |
9f271de8 | 312 | } |
e29ab294 | 313 | |
b8129910 DI |
314 | this._addMouseDownListeners(); |
315 | this.refresh(); | |
316 | } | |
317 | ||
318 | /** | |
319 | * Adds listeners when mousedown is triggered. | |
320 | */ | |
321 | private _addMouseDownListeners(): void { | |
e29ab294 DI |
322 | // Listen on the document so that dragging outside of viewport works |
323 | this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); | |
324 | this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener); | |
2621be81 | 325 | this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); |
b8129910 DI |
326 | } |
327 | ||
328 | /** | |
329 | * Removes the listeners that are registered when mousedown is triggered. | |
330 | */ | |
331 | private _removeMouseDownListeners(): void { | |
332 | this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); | |
333 | this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); | |
334 | clearInterval(this._dragScrollIntervalTimer); | |
335 | this._dragScrollIntervalTimer = null; | |
9f271de8 DI |
336 | } |
337 | ||
d0b603d0 DI |
338 | /** |
339 | * Performs a shift click, setting the selection end position to the mouse | |
340 | * position. | |
341 | * @param event The mouse event. | |
342 | */ | |
db8ded2a DI |
343 | private _onShiftClick(event: MouseEvent): void { |
344 | if (this._model.selectionStart) { | |
345 | this._model.selectionEnd = this._getMouseBufferCoords(event); | |
346 | } | |
347 | } | |
348 | ||
d0b603d0 DI |
349 | /** |
350 | * Performs a single click, resetting relevant state and setting the selection | |
351 | * start position. | |
352 | * @param event The mouse event. | |
353 | */ | |
9f271de8 | 354 | private _onSingleClick(event: MouseEvent): void { |
f7d6ab5f DI |
355 | this._model.selectionStartLength = 0; |
356 | this._model.isSelectAllActive = false; | |
5bc11121 | 357 | this._isLineSelectModeActive = false; |
f7d6ab5f DI |
358 | this._model.selectionStart = this._getMouseBufferCoords(event); |
359 | if (this._model.selectionStart) { | |
360 | this._model.selectionEnd = null; | |
54e7f65d DI |
361 | // If the mouse is over the second half of a wide character, adjust the |
362 | // selection to cover the whole character | |
363 | const char = this._buffer.get(this._model.selectionStart[1])[this._model.selectionStart[0]]; | |
364 | if (char[LINE_DATA_WIDTH_INDEX] === 0) { | |
365 | this._model.selectionStart[0]++; | |
366 | } | |
70fda994 DI |
367 | } |
368 | } | |
369 | ||
d0b603d0 DI |
370 | /** |
371 | * Performs a double click, selecting the current work. | |
372 | * @param event The mouse event. | |
373 | */ | |
9f271de8 DI |
374 | private _onDoubleClick(event: MouseEvent): void { |
375 | const coords = this._getMouseBufferCoords(event); | |
376 | if (coords) { | |
377 | this._selectWordAt(coords); | |
378 | } | |
379 | } | |
380 | ||
d0b603d0 DI |
381 | /** |
382 | * Performs a triple click, selecting the current line and activating line | |
383 | * select mode. | |
384 | * @param event The mouse event. | |
385 | */ | |
9f271de8 DI |
386 | private _onTripleClick(event: MouseEvent): void { |
387 | const coords = this._getMouseBufferCoords(event); | |
388 | if (coords) { | |
5bc11121 | 389 | this._isLineSelectModeActive = true; |
9f271de8 DI |
390 | this._selectLineAt(coords[1]); |
391 | } | |
392 | } | |
393 | ||
d0b603d0 DI |
394 | /** |
395 | * Sets the number of clicks for the current mousedown event based on the time | |
396 | * and position of the last mousedown event. | |
397 | * @param event The mouse event. | |
398 | */ | |
f380153f | 399 | private _setMouseClickCount(event: MouseEvent): void { |
9f271de8 | 400 | let currentTime = (new Date()).getTime(); |
f380153f | 401 | if (currentTime - this._lastMouseDownTime > CLEAR_MOUSE_DOWN_TIME || this._distanceFromLastMousePosition(event) > CLEAR_MOUSE_DISTANCE) { |
9f271de8 | 402 | this._clickCount = 0; |
d3865ad2 DI |
403 | } |
404 | this._lastMouseDownTime = currentTime; | |
f380153f | 405 | this._lastMousePosition = [event.pageX, event.pageY]; |
9f271de8 | 406 | this._clickCount++; |
f380153f | 407 | } |
9f271de8 | 408 | |
d0b603d0 DI |
409 | /** |
410 | * Gets the maximum number of pixels in each direction the mouse has moved. | |
411 | * @param event The mouse event. | |
412 | */ | |
f380153f DI |
413 | private _distanceFromLastMousePosition(event: MouseEvent): number { |
414 | const result = Math.max( | |
415 | Math.abs(this._lastMousePosition[0] - event.pageX), | |
416 | Math.abs(this._lastMousePosition[1] - event.pageY)); | |
417 | return result; | |
9f271de8 DI |
418 | } |
419 | ||
e63fdf58 DI |
420 | /** |
421 | * Handles the mousemove event when the mouse button is down, recording the | |
422 | * end of the selection and refreshing the selection. | |
423 | * @param event The mousemove event. | |
424 | */ | |
70fda994 | 425 | private _onMouseMove(event: MouseEvent) { |
2621be81 DI |
426 | // Record the previous position so we know whether to redraw the selection |
427 | // at the end. | |
428 | const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; | |
429 | ||
430 | // Set the initial selection end based on the mouse coordinates | |
431 | this._model.selectionEnd = this._getMouseBufferCoords(event); | |
432 | ||
433 | // Select the entire line if line select mode is active. | |
5bc11121 | 434 | if (this._isLineSelectModeActive) { |
5bc11121 DI |
435 | if (this._model.selectionEnd[1] < this._model.selectionStart[1]) { |
436 | this._model.selectionEnd[0] = 0; | |
437 | } else { | |
438 | this._model.selectionEnd[0] = this._terminal.cols; | |
439 | } | |
5bc11121 DI |
440 | } |
441 | ||
2621be81 | 442 | // Determine the amount of scrolling that will happen. |
0dc3dd03 | 443 | this._dragScrollAmount = this._getMouseEventScrollAmount(event); |
2621be81 | 444 | |
0dc3dd03 | 445 | // If the cursor was above or below the viewport, make sure it's at the |
2621be81 | 446 | // start or end of the viewport respectively. |
0dc3dd03 | 447 | if (this._dragScrollAmount > 0) { |
f7d6ab5f | 448 | this._model.selectionEnd[0] = this._terminal.cols - 1; |
0dc3dd03 | 449 | } else if (this._dragScrollAmount < 0) { |
f7d6ab5f | 450 | this._model.selectionEnd[0] = 0; |
0dc3dd03 | 451 | } |
54e7f65d DI |
452 | |
453 | // If the character is a wide character include the cell to the right in the | |
5bc11121 DI |
454 | // selection. Note that selections at the very end of the line will never |
455 | // have a character. | |
71477874 DI |
456 | if (this._model.selectionEnd[1] < this._buffer.length) { |
457 | const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; | |
458 | if (char && char[2] === 0) { | |
459 | this._model.selectionEnd[0]++; | |
460 | } | |
54e7f65d DI |
461 | } |
462 | ||
2621be81 DI |
463 | // Only draw here if the selection changes. |
464 | if (!previousSelectionEnd || | |
465 | previousSelectionEnd[0] !== this._model.selectionEnd[0] || | |
466 | previousSelectionEnd[1] !== this._model.selectionEnd[1]) { | |
467 | this.refresh(); | |
468 | } | |
70fda994 DI |
469 | } |
470 | ||
d0b603d0 DI |
471 | /** |
472 | * The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the | |
473 | * scrolling of the viewport. | |
474 | */ | |
0dc3dd03 DI |
475 | private _dragScroll() { |
476 | if (this._dragScrollAmount) { | |
477 | this._terminal.scrollDisp(this._dragScrollAmount, false); | |
478 | // Re-evaluate selection | |
479 | if (this._dragScrollAmount > 0) { | |
f7d6ab5f | 480 | this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows]; |
0dc3dd03 | 481 | } else { |
f7d6ab5f | 482 | this._model.selectionEnd = [0, this._terminal.ydisp]; |
0dc3dd03 DI |
483 | } |
484 | this.refresh(); | |
485 | } | |
486 | } | |
487 | ||
e63fdf58 | 488 | /** |
b8129910 | 489 | * Handles the mouseup event, removing the mousedown listeners. |
e63fdf58 DI |
490 | * @param event The mouseup event. |
491 | */ | |
70fda994 | 492 | private _onMouseUp(event: MouseEvent) { |
b8129910 | 493 | this._removeMouseDownListeners(); |
70fda994 | 494 | } |
597c6939 | 495 | |
cb6533f3 DI |
496 | /** |
497 | * Converts a viewport column to the character index on the buffer line, the | |
498 | * latter takes into account wide characters. | |
d3865ad2 | 499 | * @param coords The coordinates to find the 2 index for. |
cb6533f3 | 500 | */ |
11cec31d | 501 | private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number { |
cb6533f3 | 502 | let charIndex = coords[0]; |
d9991b25 | 503 | for (let i = 0; coords[0] >= i; i++) { |
11cec31d | 504 | const char = bufferLine[i]; |
cb6533f3 DI |
505 | if (char[LINE_DATA_WIDTH_INDEX] === 0) { |
506 | charIndex--; | |
507 | } | |
508 | } | |
509 | return charIndex; | |
510 | } | |
511 | ||
597c6939 DI |
512 | /** |
513 | * Selects the word at the coordinates specified. Words are defined as all | |
514 | * non-whitespace characters. | |
515 | * @param coords The coordinates to get the word at. | |
516 | */ | |
a53a0acd | 517 | protected _selectWordAt(coords: [number, number]): void { |
11cec31d DI |
518 | const bufferLine = this._buffer.get(coords[1]); |
519 | const line = this._translateBufferLineToString(bufferLine, false); | |
cb6533f3 DI |
520 | |
521 | // Get actual index, taking into consideration wide characters | |
11cec31d | 522 | let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); |
cb6533f3 | 523 | let startIndex = endIndex; |
cb6533f3 DI |
524 | |
525 | // Record offset to be used later | |
526 | const charOffset = coords[0] - startIndex; | |
11cec31d DI |
527 | let leftWideCharCount = 0; |
528 | let rightWideCharCount = 0; | |
cb6533f3 | 529 | |
cb6533f3 | 530 | if (line.charAt(startIndex) === ' ') { |
11cec31d | 531 | // Expand until non-whitespace is hit |
cb6533f3 DI |
532 | while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') { |
533 | startIndex--; | |
534 | } | |
535 | while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') { | |
536 | endIndex++; | |
537 | } | |
cb6533f3 | 538 | } else { |
11cec31d DI |
539 | // Expand until whitespace is hit. This algorithm works by scanning left |
540 | // and right from the starting position, keeping both the index format | |
541 | // (line) and the column format (bufferLine) in sync. When a wide | |
542 | // character is hit, it is recorded and the column index is adjusted. | |
d9991b25 DI |
543 | let startCol = coords[0]; |
544 | let endCol = coords[0]; | |
545 | // Consider the initial position, skip it and increment the wide char | |
546 | // variable | |
11cec31d | 547 | if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) { |
d9991b25 DI |
548 | leftWideCharCount++; |
549 | startCol--; | |
550 | } | |
11cec31d | 551 | if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) { |
d9991b25 DI |
552 | rightWideCharCount++; |
553 | endCol++; | |
554 | } | |
cb6533f3 DI |
555 | // Expand the string in both directions until a space is hit |
556 | while (startIndex > 0 && line.charAt(startIndex - 1) !== ' ') { | |
11cec31d DI |
557 | if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) { |
558 | // If the next character is a wide char, record it and skip the column | |
d9991b25 DI |
559 | leftWideCharCount++; |
560 | startCol--; | |
cb6533f3 DI |
561 | } |
562 | startIndex--; | |
d9991b25 | 563 | startCol--; |
cb6533f3 | 564 | } |
d3865ad2 | 565 | while (endIndex + 1 < line.length && line.charAt(endIndex + 1) !== ' ') { |
11cec31d DI |
566 | if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) { |
567 | // If the next character is a wide char, record it and skip the column | |
d9991b25 DI |
568 | rightWideCharCount++; |
569 | endCol++; | |
cb6533f3 DI |
570 | } |
571 | endIndex++; | |
d9991b25 | 572 | endCol++; |
cb6533f3 | 573 | } |
597c6939 | 574 | } |
11cec31d DI |
575 | |
576 | // Record the resulting selection | |
d9991b25 | 577 | this._model.selectionStart = [startIndex + charOffset - leftWideCharCount, coords[1]]; |
f61d852f | 578 | this._model.selectionStartLength = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols); |
597c6939 | 579 | } |
9f271de8 | 580 | |
d0b603d0 DI |
581 | /** |
582 | * Selects the line specified. | |
583 | * @param line The line index. | |
584 | */ | |
fd91c5e1 | 585 | protected _selectLineAt(line: number): void { |
f7d6ab5f DI |
586 | this._model.selectionStart = [0, line]; |
587 | this._model.selectionStartLength = this._terminal.cols; | |
9f271de8 | 588 | } |
70fda994 | 589 | } |