]>
Commit | Line | Data |
---|---|---|
70fda994 DI |
1 | /** |
2 | * @license MIT | |
3 | */ | |
4 | ||
dc165175 DI |
5 | import * as Mouse from './utils/Mouse'; |
6 | import * as Browser from './utils/Browser'; | |
70fda994 DI |
7 | import { CharMeasure } from './utils/CharMeasure'; |
8 | import { CircularList } from './utils/CircularList'; | |
b594407c | 9 | import { EventEmitter } from './EventEmitter'; |
59f75555 | 10 | import { ITerminal, ICircularList } from './Interfaces'; |
f7d6ab5f | 11 | import { SelectionModel } from './SelectionModel'; |
b9571307 | 12 | import { translateBufferLineToString } from './utils/BufferLine'; |
70fda994 | 13 | |
0dc3dd03 DI |
14 | /** |
15 | * The number of pixels the mouse needs to be above or below the viewport in | |
16 | * order to scroll at the maximum speed. | |
17 | */ | |
b8129910 | 18 | const DRAG_SCROLL_MAX_THRESHOLD = 50; |
0dc3dd03 DI |
19 | |
20 | /** | |
21 | * The maximum scrolling speed | |
22 | */ | |
b8129910 | 23 | const DRAG_SCROLL_MAX_SPEED = 15; |
0dc3dd03 DI |
24 | |
25 | /** | |
26 | * The number of milliseconds between drag scroll updates. | |
27 | */ | |
b8129910 | 28 | const DRAG_SCROLL_INTERVAL = 50; |
0dc3dd03 | 29 | |
7f2e94c3 DI |
30 | /** |
31 | * A string containing all characters that are considered word separated by the | |
32 | * double click to select work logic. | |
33 | */ | |
ca03f892 | 34 | const WORD_SEPARATORS = ' ()[]{}\'"'; |
7f2e94c3 | 35 | |
2621be81 DI |
36 | // TODO: Move these constants elsewhere, they belong in a buffer or buffer |
37 | // data/line class. | |
54e7f65d DI |
38 | const LINE_DATA_CHAR_INDEX = 1; |
39 | const LINE_DATA_WIDTH_INDEX = 2; | |
40 | ||
2012c8c9 DI |
41 | const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160); |
42 | const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g'); | |
43 | ||
320fb55d DI |
44 | /** |
45 | * Represents a position of a word on a line. | |
46 | */ | |
4b170ca4 DI |
47 | interface IWordPosition { |
48 | start: number; | |
49 | length: number; | |
50 | } | |
51 | ||
320fb55d DI |
52 | /** |
53 | * A selection mode, this drives how the selection behaves on mouse move. | |
54 | */ | |
55 | enum SelectionMode { | |
56 | NORMAL, | |
57 | WORD, | |
58 | LINE | |
59 | } | |
60 | ||
6075498c DI |
61 | /** |
62 | * A class that manages the selection of the terminal. With help from | |
63 | * SelectionModel, SelectionManager handles with all logic associated with | |
64 | * dealing with the selection, including handling mouse interaction, wide | |
65 | * characters and fetching the actual text within the selection. Rendering is | |
66 | * not handled by the SelectionManager but a 'refresh' event is fired when the | |
67 | * selection is ready to be redrawn. | |
68 | */ | |
b594407c | 69 | export class SelectionManager extends EventEmitter { |
fd91c5e1 | 70 | protected _model: SelectionModel; |
9f271de8 DI |
71 | |
72 | /** | |
73 | * The amount to scroll every drag scroll update (depends on how far the mouse | |
74 | * drag is above or below the terminal). | |
75 | */ | |
0dc3dd03 | 76 | private _dragScrollAmount: number; |
70fda994 | 77 | |
2621be81 | 78 | /** |
320fb55d | 79 | * The current selection mode. |
2621be81 | 80 | */ |
320fb55d | 81 | private _activeSelectionMode: SelectionMode; |
5bc11121 | 82 | |
d0b603d0 DI |
83 | /** |
84 | * A setInterval timer that is active while the mouse is down whose callback | |
85 | * scrolls the viewport when necessary. | |
86 | */ | |
87 | private _dragScrollIntervalTimer: NodeJS.Timer; | |
88 | ||
b81c165b DI |
89 | /** |
90 | * The animation frame ID used for refreshing the selection. | |
91 | */ | |
13c401cb DI |
92 | private _refreshAnimationFrame: number; |
93 | ||
80a83ff8 JM |
94 | /** |
95 | * Whether selection is enabled. | |
96 | */ | |
97 | private _enabled = true; | |
98 | ||
70fda994 | 99 | private _mouseMoveListener: EventListener; |
ab40908f | 100 | private _mouseUpListener: EventListener; |
70fda994 | 101 | |
ad3ae67e DI |
102 | constructor( |
103 | private _terminal: ITerminal, | |
59f75555 | 104 | private _buffer: ICircularList<[number, string, number][]>, |
ad3ae67e | 105 | private _rowContainer: HTMLElement, |
ad3ae67e DI |
106 | private _charMeasure: CharMeasure |
107 | ) { | |
b594407c | 108 | super(); |
ab40908f DI |
109 | this._initListeners(); |
110 | this.enable(); | |
9f271de8 | 111 | |
f7d6ab5f | 112 | this._model = new SelectionModel(_terminal); |
320fb55d | 113 | this._activeSelectionMode = SelectionMode.NORMAL; |
70fda994 DI |
114 | } |
115 | ||
d0b603d0 DI |
116 | /** |
117 | * Initializes listener variables. | |
118 | */ | |
ab40908f | 119 | private _initListeners() { |
70fda994 | 120 | this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event); |
ab40908f | 121 | this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event); |
80a83ff8 JM |
122 | |
123 | this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(<MouseEvent>event)); | |
124 | ||
125 | // Only adjust the selection on trim, shiftElements is rarely used (only in | |
126 | // reverseIndex) and delete in a splice is only ever used when the same | |
127 | // number of elements was just added. Given this is could actually be | |
128 | // beneficial to leave the selection as is for these cases. | |
129 | this._buffer.on('trim', (amount: number) => this._onTrim(amount)); | |
ab40908f | 130 | } |
70fda994 | 131 | |
ab40908f DI |
132 | /** |
133 | * Disables the selection manager. This is useful for when terminal mouse | |
134 | * are enabled. | |
135 | */ | |
136 | public disable() { | |
4405f5e1 | 137 | this.clearSelection(); |
80a83ff8 | 138 | this._enabled = false; |
ab40908f DI |
139 | } |
140 | ||
141 | /** | |
142 | * Enable the selection manager. | |
143 | */ | |
144 | public enable() { | |
80a83ff8 | 145 | this._enabled = true; |
70fda994 DI |
146 | } |
147 | ||
9a86eeb3 DI |
148 | /** |
149 | * Sets the active buffer, this should be called when the alt buffer is | |
150 | * switched in or out. | |
151 | * @param buffer The active buffer. | |
152 | */ | |
59f75555 | 153 | public setBuffer(buffer: ICircularList<[number, string, number][]>): void { |
9a86eeb3 | 154 | this._buffer = buffer; |
645a5920 | 155 | this.clearSelection(); |
9a86eeb3 DI |
156 | } |
157 | ||
280b698a DI |
158 | public get selectionStart(): [number, number] { return this._model.finalSelectionStart; } |
159 | public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; } | |
160 | ||
1343b83f DI |
161 | /** |
162 | * Gets whether there is an active text selection. | |
163 | */ | |
164 | public get hasSelection(): boolean { | |
7b469407 DI |
165 | const start = this._model.finalSelectionStart; |
166 | const end = this._model.finalSelectionEnd; | |
167 | if (!start || !end) { | |
168 | return false; | |
169 | } | |
170 | return start[0] !== end[0] || start[1] !== end[1]; | |
1343b83f DI |
171 | } |
172 | ||
9f271de8 DI |
173 | /** |
174 | * Gets the text currently selected. | |
175 | */ | |
70fda994 | 176 | public get selectionText(): string { |
f7d6ab5f DI |
177 | const start = this._model.finalSelectionStart; |
178 | const end = this._model.finalSelectionEnd; | |
e29ab294 | 179 | if (!start || !end) { |
293ae18a | 180 | return ''; |
70fda994 | 181 | } |
43c796a7 | 182 | |
43c796a7 | 183 | // Get first row |
293ae18a | 184 | const startRowEndCol = start[1] === end[1] ? end[0] : null; |
32b34cbe | 185 | let result: string[] = []; |
b9571307 | 186 | result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol)); |
43c796a7 DI |
187 | |
188 | // Get middle rows | |
293ae18a | 189 | for (let i = start[1] + 1; i <= end[1] - 1; i++) { |
346b5177 | 190 | const bufferLine = this._buffer.get(i); |
53602775 | 191 | const lineText = translateBufferLineToString(bufferLine, true); |
59f75555 | 192 | if ((<any>bufferLine).isWrapped) { |
346b5177 DI |
193 | result[result.length - 1] += lineText; |
194 | } else { | |
195 | result.push(lineText); | |
196 | } | |
32b34cbe | 197 | } |
43c796a7 DI |
198 | |
199 | // Get final row | |
293ae18a | 200 | if (start[1] !== end[1]) { |
346b5177 | 201 | const bufferLine = this._buffer.get(end[1]); |
53602775 | 202 | const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]); |
59f75555 | 203 | if ((<any>bufferLine).isWrapped) { |
346b5177 DI |
204 | result[result.length - 1] += lineText; |
205 | } else { | |
206 | result.push(lineText); | |
207 | } | |
32b34cbe | 208 | } |
2b243182 | 209 | |
2012c8c9 DI |
210 | // Format string by replacing non-breaking space chars with regular spaces |
211 | // and joining the array into a multi-line string. | |
212 | const formattedResult = result.map(line => { | |
213 | return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' '); | |
cfe22e6f | 214 | }).join(Browser.isMSWindows ? '\r\n' : '\n'); |
2012c8c9 DI |
215 | |
216 | return formattedResult; | |
32b34cbe DI |
217 | } |
218 | ||
5d33727a DI |
219 | /** |
220 | * Clears the current terminal selection. | |
221 | */ | |
222 | public clearSelection(): void { | |
4405f5e1 | 223 | this._model.clearSelection(); |
5d33727a DI |
224 | this._removeMouseDownListeners(); |
225 | this.refresh(); | |
226 | } | |
227 | ||
207c4cf9 | 228 | /** |
13c401cb | 229 | * Queues a refresh, redrawing the selection on the next opportunity. |
63d63c27 DI |
230 | * @param isNewSelection Whether the selection should be registered as a new |
231 | * selection on Linux. | |
207c4cf9 | 232 | */ |
63d63c27 | 233 | public refresh(isNewSelection?: boolean): void { |
8811d96a | 234 | // Queue the refresh for the renderer |
13c401cb DI |
235 | if (!this._refreshAnimationFrame) { |
236 | this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); | |
237 | } | |
8811d96a | 238 | |
dc165175 DI |
239 | // If the platform is Linux and the refresh call comes from a mouse event, |
240 | // we need to update the selection for middle click to paste selection. | |
63d63c27 | 241 | if (Browser.isLinux && isNewSelection) { |
dc165175 DI |
242 | const selectionText = this.selectionText; |
243 | if (selectionText.length) { | |
244 | this.emit('newselection', this.selectionText); | |
245 | } | |
8811d96a | 246 | } |
13c401cb DI |
247 | } |
248 | ||
249 | /** | |
250 | * Fires the refresh event, causing consumers to pick it up and redraw the | |
251 | * selection state. | |
252 | */ | |
253 | private _refresh(): void { | |
254 | this._refreshAnimationFrame = null; | |
f7d6ab5f | 255 | this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd }); |
25152e44 DI |
256 | } |
257 | ||
258 | /** | |
259 | * Selects all text within the terminal. | |
260 | */ | |
261 | public selectAll(): void { | |
f7d6ab5f | 262 | this._model.isSelectAllActive = true; |
25152e44 | 263 | this.refresh(); |
207c4cf9 DI |
264 | } |
265 | ||
266 | /** | |
267 | * Handle the buffer being trimmed, adjust the selection position. | |
268 | * @param amount The amount the buffer is being trimmed. | |
269 | */ | |
70fda994 | 270 | private _onTrim(amount: number) { |
f7d6ab5f DI |
271 | const needsRefresh = this._model.onTrim(amount); |
272 | if (needsRefresh) { | |
207c4cf9 | 273 | this.refresh(); |
207c4cf9 | 274 | } |
70fda994 DI |
275 | } |
276 | ||
d0b603d0 DI |
277 | /** |
278 | * Gets the 0-based [x, y] buffer coordinates of the current mouse event. | |
279 | * @param event The mouse event. | |
280 | */ | |
0dc3dd03 | 281 | private _getMouseBufferCoords(event: MouseEvent): [number, number] { |
b05814e6 | 282 | const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true); |
e56f710c DI |
283 | if (!coords) { |
284 | return null; | |
285 | } | |
286 | ||
ad3ae67e DI |
287 | // Convert to 0-based |
288 | coords[0]--; | |
289 | coords[1]--; | |
290 | // Convert viewport coords to buffer coords | |
bbafdd3d | 291 | coords[1] += this._terminal.buffer.ydisp; |
ad3ae67e | 292 | return coords; |
b36d8780 DI |
293 | } |
294 | ||
d0b603d0 DI |
295 | /** |
296 | * Gets the amount the viewport should be scrolled based on how far out of the | |
297 | * terminal the mouse is. | |
298 | * @param event The mouse event. | |
299 | */ | |
0dc3dd03 DI |
300 | private _getMouseEventScrollAmount(event: MouseEvent): number { |
301 | let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1]; | |
302 | const terminalHeight = this._terminal.rows * this._charMeasure.height; | |
303 | if (offset >= 0 && offset <= terminalHeight) { | |
304 | return 0; | |
305 | } | |
306 | if (offset > terminalHeight) { | |
307 | offset -= terminalHeight; | |
308 | } | |
309 | ||
310 | offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD); | |
311 | offset /= DRAG_SCROLL_MAX_THRESHOLD; | |
312 | return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1)); | |
313 | } | |
314 | ||
e63fdf58 DI |
315 | /** |
316 | * Handles te mousedown event, setting up for a new selection. | |
317 | * @param event The mousedown event. | |
318 | */ | |
70fda994 | 319 | private _onMouseDown(event: MouseEvent) { |
3479614c DI |
320 | // If we have selection, we want the context menu on right click even if the |
321 | // terminal is in mouse mode. | |
6a3b39b4 JM |
322 | if (event.button === 2 && this.hasSelection) { |
323 | event.stopPropagation(); | |
324 | return; | |
80a83ff8 JM |
325 | } |
326 | ||
0dc3dd03 DI |
327 | // Only action the primary button |
328 | if (event.button !== 0) { | |
329 | return; | |
330 | } | |
331 | ||
6a3b39b4 JM |
332 | // Allow selection when using a specific modifier key, even when disabled |
333 | if (!this._enabled) { | |
3479614c | 334 | const shouldForceSelection = Browser.isMac && event.altKey; |
6a3b39b4 JM |
335 | |
336 | if (!shouldForceSelection) { | |
337 | return; | |
338 | } | |
339 | ||
340 | // Don't send the mouse down event to the current process, we want to select | |
341 | event.stopPropagation(); | |
342 | } | |
343 | ||
2a0f44b0 DI |
344 | // Tell the browser not to start a regular selection |
345 | event.preventDefault(); | |
346 | ||
b8129910 DI |
347 | // Reset drag scroll state |
348 | this._dragScrollAmount = 0; | |
349 | ||
0e0ecc2d JM |
350 | if (this._enabled && event.shiftKey) { |
351 | this._onIncrementalClick(event); | |
db8ded2a | 352 | } else { |
9bd52820 | 353 | if (event.detail === 1) { |
80a83ff8 | 354 | this._onSingleClick(event); |
9bd52820 | 355 | } else if (event.detail === 2) { |
80a83ff8 | 356 | this._onDoubleClick(event); |
9bd52820 | 357 | } else if (event.detail === 3) { |
80a83ff8 | 358 | this._onTripleClick(event); |
db8ded2a | 359 | } |
9f271de8 | 360 | } |
e29ab294 | 361 | |
b8129910 | 362 | this._addMouseDownListeners(); |
8811d96a | 363 | this.refresh(true); |
b8129910 DI |
364 | } |
365 | ||
366 | /** | |
367 | * Adds listeners when mousedown is triggered. | |
368 | */ | |
369 | private _addMouseDownListeners(): void { | |
e29ab294 DI |
370 | // Listen on the document so that dragging outside of viewport works |
371 | this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); | |
372 | this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener); | |
2621be81 | 373 | this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); |
b8129910 DI |
374 | } |
375 | ||
376 | /** | |
377 | * Removes the listeners that are registered when mousedown is triggered. | |
378 | */ | |
379 | private _removeMouseDownListeners(): void { | |
380 | this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); | |
381 | this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); | |
382 | clearInterval(this._dragScrollIntervalTimer); | |
383 | this._dragScrollIntervalTimer = null; | |
9f271de8 DI |
384 | } |
385 | ||
d0b603d0 | 386 | /** |
0e0ecc2d | 387 | * Performs an incremental click, setting the selection end position to the mouse |
d0b603d0 DI |
388 | * position. |
389 | * @param event The mouse event. | |
390 | */ | |
0e0ecc2d | 391 | private _onIncrementalClick(event: MouseEvent): void { |
db8ded2a DI |
392 | if (this._model.selectionStart) { |
393 | this._model.selectionEnd = this._getMouseBufferCoords(event); | |
394 | } | |
395 | } | |
396 | ||
d0b603d0 DI |
397 | /** |
398 | * Performs a single click, resetting relevant state and setting the selection | |
399 | * start position. | |
400 | * @param event The mouse event. | |
401 | */ | |
9f271de8 | 402 | private _onSingleClick(event: MouseEvent): void { |
f7d6ab5f DI |
403 | this._model.selectionStartLength = 0; |
404 | this._model.isSelectAllActive = false; | |
320fb55d | 405 | this._activeSelectionMode = SelectionMode.NORMAL; |
e56f710c DI |
406 | |
407 | // Initialize the new selection | |
f7d6ab5f | 408 | this._model.selectionStart = this._getMouseBufferCoords(event); |
e56f710c DI |
409 | if (!this._model.selectionStart) { |
410 | return; | |
411 | } | |
412 | this._model.selectionEnd = null; | |
413 | ||
414 | // Ensure the line exists | |
415 | const line = this._buffer.get(this._model.selectionStart[1]); | |
416 | if (!line) { | |
417 | return; | |
418 | } | |
419 | ||
420 | // If the mouse is over the second half of a wide character, adjust the | |
421 | // selection to cover the whole character | |
422 | const char = line[this._model.selectionStart[0]]; | |
423 | if (char[LINE_DATA_WIDTH_INDEX] === 0) { | |
424 | this._model.selectionStart[0]++; | |
70fda994 DI |
425 | } |
426 | } | |
427 | ||
d0b603d0 DI |
428 | /** |
429 | * Performs a double click, selecting the current work. | |
430 | * @param event The mouse event. | |
431 | */ | |
9f271de8 DI |
432 | private _onDoubleClick(event: MouseEvent): void { |
433 | const coords = this._getMouseBufferCoords(event); | |
434 | if (coords) { | |
320fb55d | 435 | this._activeSelectionMode = SelectionMode.WORD; |
9f271de8 DI |
436 | this._selectWordAt(coords); |
437 | } | |
438 | } | |
439 | ||
d0b603d0 DI |
440 | /** |
441 | * Performs a triple click, selecting the current line and activating line | |
442 | * select mode. | |
443 | * @param event The mouse event. | |
444 | */ | |
9f271de8 DI |
445 | private _onTripleClick(event: MouseEvent): void { |
446 | const coords = this._getMouseBufferCoords(event); | |
447 | if (coords) { | |
320fb55d | 448 | this._activeSelectionMode = SelectionMode.LINE; |
9f271de8 DI |
449 | this._selectLineAt(coords[1]); |
450 | } | |
451 | } | |
452 | ||
e63fdf58 DI |
453 | /** |
454 | * Handles the mousemove event when the mouse button is down, recording the | |
455 | * end of the selection and refreshing the selection. | |
456 | * @param event The mousemove event. | |
457 | */ | |
70fda994 | 458 | private _onMouseMove(event: MouseEvent) { |
2621be81 DI |
459 | // Record the previous position so we know whether to redraw the selection |
460 | // at the end. | |
461 | const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; | |
462 | ||
463 | // Set the initial selection end based on the mouse coordinates | |
464 | this._model.selectionEnd = this._getMouseBufferCoords(event); | |
e56f710c DI |
465 | if (!this._model.selectionEnd) { |
466 | this.refresh(true); | |
467 | return; | |
468 | } | |
2621be81 DI |
469 | |
470 | // Select the entire line if line select mode is active. | |
320fb55d | 471 | if (this._activeSelectionMode === SelectionMode.LINE) { |
5bc11121 DI |
472 | if (this._model.selectionEnd[1] < this._model.selectionStart[1]) { |
473 | this._model.selectionEnd[0] = 0; | |
474 | } else { | |
475 | this._model.selectionEnd[0] = this._terminal.cols; | |
476 | } | |
320fb55d | 477 | } else if (this._activeSelectionMode === SelectionMode.WORD) { |
4b170ca4 | 478 | this._selectToWordAt(this._model.selectionEnd); |
5bc11121 DI |
479 | } |
480 | ||
2621be81 | 481 | // Determine the amount of scrolling that will happen. |
0dc3dd03 | 482 | this._dragScrollAmount = this._getMouseEventScrollAmount(event); |
2621be81 | 483 | |
0dc3dd03 | 484 | // If the cursor was above or below the viewport, make sure it's at the |
2621be81 | 485 | // start or end of the viewport respectively. |
0dc3dd03 | 486 | if (this._dragScrollAmount > 0) { |
f7d6ab5f | 487 | this._model.selectionEnd[0] = this._terminal.cols - 1; |
0dc3dd03 | 488 | } else if (this._dragScrollAmount < 0) { |
f7d6ab5f | 489 | this._model.selectionEnd[0] = 0; |
0dc3dd03 | 490 | } |
54e7f65d DI |
491 | |
492 | // If the character is a wide character include the cell to the right in the | |
5bc11121 DI |
493 | // selection. Note that selections at the very end of the line will never |
494 | // have a character. | |
71477874 DI |
495 | if (this._model.selectionEnd[1] < this._buffer.length) { |
496 | const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; | |
497 | if (char && char[2] === 0) { | |
498 | this._model.selectionEnd[0]++; | |
499 | } | |
54e7f65d DI |
500 | } |
501 | ||
2621be81 DI |
502 | // Only draw here if the selection changes. |
503 | if (!previousSelectionEnd || | |
80a83ff8 JM |
504 | previousSelectionEnd[0] !== this._model.selectionEnd[0] || |
505 | previousSelectionEnd[1] !== this._model.selectionEnd[1]) { | |
dc165175 | 506 | this.refresh(true); |
2621be81 | 507 | } |
70fda994 DI |
508 | } |
509 | ||
d0b603d0 DI |
510 | /** |
511 | * The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the | |
512 | * scrolling of the viewport. | |
513 | */ | |
0dc3dd03 DI |
514 | private _dragScroll() { |
515 | if (this._dragScrollAmount) { | |
516 | this._terminal.scrollDisp(this._dragScrollAmount, false); | |
517 | // Re-evaluate selection | |
518 | if (this._dragScrollAmount > 0) { | |
bbafdd3d | 519 | this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows]; |
0dc3dd03 | 520 | } else { |
bbafdd3d | 521 | this._model.selectionEnd = [0, this._terminal.buffer.ydisp]; |
0dc3dd03 DI |
522 | } |
523 | this.refresh(); | |
524 | } | |
525 | } | |
526 | ||
e63fdf58 | 527 | /** |
b8129910 | 528 | * Handles the mouseup event, removing the mousedown listeners. |
e63fdf58 DI |
529 | * @param event The mouseup event. |
530 | */ | |
70fda994 | 531 | private _onMouseUp(event: MouseEvent) { |
b8129910 | 532 | this._removeMouseDownListeners(); |
70fda994 | 533 | } |
597c6939 | 534 | |
cb6533f3 DI |
535 | /** |
536 | * Converts a viewport column to the character index on the buffer line, the | |
537 | * latter takes into account wide characters. | |
d3865ad2 | 538 | * @param coords The coordinates to find the 2 index for. |
cb6533f3 | 539 | */ |
11cec31d | 540 | private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number { |
cb6533f3 | 541 | let charIndex = coords[0]; |
d9991b25 | 542 | for (let i = 0; coords[0] >= i; i++) { |
11cec31d | 543 | const char = bufferLine[i]; |
cb6533f3 DI |
544 | if (char[LINE_DATA_WIDTH_INDEX] === 0) { |
545 | charIndex--; | |
546 | } | |
547 | } | |
548 | return charIndex; | |
549 | } | |
550 | ||
8d12881a DI |
551 | public setSelection(col: number, row: number, length: number): void { |
552 | this._model.clearSelection(); | |
553 | this._removeMouseDownListeners(); | |
8d12881a DI |
554 | this._model.selectionStart = [col, row]; |
555 | this._model.selectionStartLength = length; | |
556 | this.refresh(); | |
557 | } | |
558 | ||
597c6939 | 559 | /** |
4b170ca4 | 560 | * Gets positional information for the word at the coordinated specified. |
597c6939 DI |
561 | * @param coords The coordinates to get the word at. |
562 | */ | |
4b170ca4 | 563 | private _getWordAt(coords: [number, number]): IWordPosition { |
11cec31d | 564 | const bufferLine = this._buffer.get(coords[1]); |
e3ded611 DI |
565 | if (!bufferLine) { |
566 | return null; | |
567 | } | |
568 | ||
b9571307 | 569 | const line = translateBufferLineToString(bufferLine, false); |
cb6533f3 DI |
570 | |
571 | // Get actual index, taking into consideration wide characters | |
11cec31d | 572 | let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); |
cb6533f3 | 573 | let startIndex = endIndex; |
cb6533f3 DI |
574 | |
575 | // Record offset to be used later | |
576 | const charOffset = coords[0] - startIndex; | |
11cec31d DI |
577 | let leftWideCharCount = 0; |
578 | let rightWideCharCount = 0; | |
cb6533f3 | 579 | |
cb6533f3 | 580 | if (line.charAt(startIndex) === ' ') { |
11cec31d | 581 | // Expand until non-whitespace is hit |
cb6533f3 DI |
582 | while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') { |
583 | startIndex--; | |
584 | } | |
585 | while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') { | |
586 | endIndex++; | |
587 | } | |
cb6533f3 | 588 | } else { |
11cec31d DI |
589 | // Expand until whitespace is hit. This algorithm works by scanning left |
590 | // and right from the starting position, keeping both the index format | |
591 | // (line) and the column format (bufferLine) in sync. When a wide | |
592 | // character is hit, it is recorded and the column index is adjusted. | |
d9991b25 DI |
593 | let startCol = coords[0]; |
594 | let endCol = coords[0]; | |
595 | // Consider the initial position, skip it and increment the wide char | |
596 | // variable | |
11cec31d | 597 | if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) { |
d9991b25 DI |
598 | leftWideCharCount++; |
599 | startCol--; | |
600 | } | |
11cec31d | 601 | if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) { |
d9991b25 DI |
602 | rightWideCharCount++; |
603 | endCol++; | |
604 | } | |
cb6533f3 | 605 | // Expand the string in both directions until a space is hit |
7f2e94c3 | 606 | while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { |
11cec31d DI |
607 | if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) { |
608 | // If the next character is a wide char, record it and skip the column | |
d9991b25 DI |
609 | leftWideCharCount++; |
610 | startCol--; | |
cb6533f3 DI |
611 | } |
612 | startIndex--; | |
d9991b25 | 613 | startCol--; |
cb6533f3 | 614 | } |
7f2e94c3 | 615 | while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { |
11cec31d DI |
616 | if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) { |
617 | // If the next character is a wide char, record it and skip the column | |
d9991b25 DI |
618 | rightWideCharCount++; |
619 | endCol++; | |
cb6533f3 DI |
620 | } |
621 | endIndex++; | |
d9991b25 | 622 | endCol++; |
cb6533f3 | 623 | } |
597c6939 | 624 | } |
11cec31d | 625 | |
4b170ca4 DI |
626 | const start = startIndex + charOffset - leftWideCharCount; |
627 | const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols); | |
80a83ff8 | 628 | return { start, length }; |
4b170ca4 DI |
629 | } |
630 | ||
631 | /** | |
632 | * Selects the word at the coordinates specified. | |
633 | * @param coords The coordinates to get the word at. | |
634 | */ | |
635 | protected _selectWordAt(coords: [number, number]): void { | |
636 | const wordPosition = this._getWordAt(coords); | |
e3ded611 DI |
637 | if (wordPosition) { |
638 | this._model.selectionStart = [wordPosition.start, coords[1]]; | |
639 | this._model.selectionStartLength = wordPosition.length; | |
640 | } | |
4b170ca4 DI |
641 | } |
642 | ||
643 | /** | |
644 | * Sets the selection end to the word at the coordinated specified. | |
645 | * @param coords The coordinates to get the word at. | |
646 | */ | |
647 | private _selectToWordAt(coords: [number, number]): void { | |
648 | const wordPosition = this._getWordAt(coords); | |
e3ded611 DI |
649 | if (wordPosition) { |
650 | this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]]; | |
651 | } | |
597c6939 | 652 | } |
9f271de8 | 653 | |
7f2e94c3 DI |
654 | /** |
655 | * Gets whether the character is considered a word separator by the select | |
656 | * word logic. | |
657 | * @param char The character to check. | |
658 | */ | |
659 | private _isCharWordSeparator(char: string): boolean { | |
660 | return WORD_SEPARATORS.indexOf(char) >= 0; | |
597c6939 | 661 | } |
9f271de8 | 662 | |
d0b603d0 DI |
663 | /** |
664 | * Selects the line specified. | |
665 | * @param line The line index. | |
666 | */ | |
fd91c5e1 | 667 | protected _selectLineAt(line: number): void { |
f7d6ab5f DI |
668 | this._model.selectionStart = [0, line]; |
669 | this._model.selectionStartLength = this._terminal.cols; | |
9f271de8 | 670 | } |
70fda994 | 671 | } |