]>
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'; |
70fda994 | 10 | |
b594407c DI |
11 | export class SelectionManager extends EventEmitter { |
12 | // TODO: Create a SelectionModel | |
70fda994 DI |
13 | private _selectionStart: [number, number]; |
14 | private _selectionEnd: [number, number]; | |
15 | ||
70fda994 DI |
16 | private _mouseMoveListener: EventListener; |
17 | ||
ad3ae67e DI |
18 | constructor( |
19 | private _terminal: ITerminal, | |
20 | private _buffer: CircularList<any>, | |
21 | private _rowContainer: HTMLElement, | |
22 | private _selectionContainer: HTMLElement, | |
23 | private _charMeasure: CharMeasure | |
24 | ) { | |
b594407c | 25 | super(); |
70fda994 DI |
26 | this._attachListeners(); |
27 | } | |
28 | ||
29 | private _attachListeners() { | |
30 | this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event); | |
31 | ||
32 | this._buffer.on('trim', amount => this._onTrim(amount)); | |
33 | this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(event)); | |
34 | this._rowContainer.addEventListener('mouseup', event => this._onMouseUp(event)); | |
597c6939 | 35 | this._rowContainer.addEventListener('dblclick', event => this._onDblclick(event)); |
70fda994 DI |
36 | } |
37 | ||
38 | public get selectionText(): string { | |
39 | if (!this._selectionStart || !this._selectionEnd) { | |
293ae18a | 40 | return ''; |
70fda994 | 41 | } |
293ae18a DI |
42 | const flipValues = this._selectionStart[1] > this._selectionEnd[1] || |
43 | (this._selectionStart[1] === this._selectionEnd[1] && this._selectionStart[0] > this._selectionEnd[0]); | |
44 | const start = flipValues ? this._selectionEnd : this._selectionStart; | |
45 | const end = flipValues ? this._selectionStart : this._selectionEnd; | |
46 | const startRowEndCol = start[1] === end[1] ? end[0] : null; | |
32b34cbe | 47 | let result: string[] = []; |
293ae18a DI |
48 | result.push(this._translateBufferLineToString(this._buffer.get(start[1]), start[0], startRowEndCol)); |
49 | for (let i = start[1] + 1; i <= end[1] - 1; i++) { | |
32b34cbe DI |
50 | result.push(this._translateBufferLineToString(this._buffer.get(i))); |
51 | } | |
293ae18a DI |
52 | if (start[1] !== end[1]) { |
53 | result.push(this._translateBufferLineToString(this._buffer.get(end[1]), 0, end[1])); | |
32b34cbe | 54 | } |
597c6939 | 55 | console.log('selectionText result: ' + result); |
32b34cbe DI |
56 | return result.join('\n'); |
57 | } | |
58 | ||
59 | private _translateBufferLineToString(line: any, startCol: number = 0, endCol: number = null): string { | |
60 | // TODO: This function should live in a buffer or buffer line class | |
61 | endCol = endCol || line.length | |
62 | let result = ''; | |
63 | for (let i = startCol; i < endCol; i++) { | |
64 | result += line[i][1]; | |
65 | } | |
597c6939 | 66 | // TODO: Trim line here instead of in handlers/Clipboard? |
e63fdf58 | 67 | // TODO: Only trim off the whitespace at the end of a line |
32b34cbe | 68 | // TODO: Handle the double-width character case |
e63fdf58 | 69 | return result; |
70fda994 DI |
70 | } |
71 | ||
207c4cf9 DI |
72 | /** |
73 | * Redraws the selection. | |
74 | */ | |
75 | public refresh(): void { | |
b594407c DI |
76 | // TODO: Figure out when to refresh the selection vs when to refresh the viewport |
77 | this.emit('refresh', { start: this._selectionStart, end: this._selectionEnd }); | |
207c4cf9 DI |
78 | } |
79 | ||
80 | /** | |
81 | * Handle the buffer being trimmed, adjust the selection position. | |
82 | * @param amount The amount the buffer is being trimmed. | |
83 | */ | |
70fda994 | 84 | private _onTrim(amount: number) { |
207c4cf9 DI |
85 | // Adjust the selection position based on the trimmed amount. |
86 | this._selectionStart[0] -= amount; | |
87 | this._selectionEnd[0] -= amount; | |
88 | ||
89 | // The selection has moved off the buffer, clear it. | |
90 | if (this._selectionEnd[0] < 0) { | |
91 | this._selectionStart = null; | |
92 | this._selectionEnd = null; | |
93 | this.refresh(); | |
94 | return; | |
95 | } | |
96 | ||
97 | // If the selection start is trimmed, ensure the start column is 0. | |
98 | if (this._selectionStart[0] < 0) { | |
99 | this._selectionStart[1] = 0; | |
100 | } | |
70fda994 DI |
101 | } |
102 | ||
32b34cbe DI |
103 | // TODO: Handle splice/shiftElements in the buffer (just clear the selection?) |
104 | ||
b36d8780 | 105 | private _getMouseBufferCoords(event: MouseEvent) { |
ad3ae67e DI |
106 | const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure); |
107 | // Convert to 0-based | |
108 | coords[0]--; | |
109 | coords[1]--; | |
110 | // Convert viewport coords to buffer coords | |
111 | coords[1] += this._terminal.ydisp; | |
112 | return coords; | |
b36d8780 DI |
113 | } |
114 | ||
e63fdf58 DI |
115 | /** |
116 | * Handles te mousedown event, setting up for a new selection. | |
117 | * @param event The mousedown event. | |
118 | */ | |
70fda994 | 119 | private _onMouseDown(event: MouseEvent) { |
b36d8780 | 120 | this._selectionStart = this._getMouseBufferCoords(event); |
70fda994 | 121 | if (this._selectionStart) { |
ad3ae67e | 122 | this._selectionEnd = null; |
70fda994 | 123 | this._rowContainer.addEventListener('mousemove', this._mouseMoveListener); |
ad3ae67e | 124 | this.refresh(); |
70fda994 DI |
125 | } |
126 | } | |
127 | ||
e63fdf58 DI |
128 | /** |
129 | * Handles the mousemove event when the mouse button is down, recording the | |
130 | * end of the selection and refreshing the selection. | |
131 | * @param event The mousemove event. | |
132 | */ | |
70fda994 | 133 | private _onMouseMove(event: MouseEvent) { |
b36d8780 | 134 | this._selectionEnd = this._getMouseBufferCoords(event); |
207c4cf9 DI |
135 | // TODO: Only draw here if the selection changes |
136 | this.refresh(); | |
70fda994 DI |
137 | } |
138 | ||
e63fdf58 DI |
139 | /** |
140 | * Handles the mouseup event, removing the mousemove listener when | |
141 | * appropriate. | |
142 | * @param event The mouseup event. | |
143 | */ | |
70fda994 | 144 | private _onMouseUp(event: MouseEvent) { |
70fda994 DI |
145 | if (!this._selectionStart) { |
146 | return; | |
147 | } | |
148 | this._rowContainer.removeEventListener('mousemove', this._mouseMoveListener); | |
149 | } | |
597c6939 DI |
150 | |
151 | private _onDblclick(event: MouseEvent) { | |
152 | const coords = this._getMouseBufferCoords(event); | |
153 | if (coords) { | |
154 | this._selectWordAt(coords); | |
155 | } | |
156 | } | |
157 | ||
158 | /** | |
159 | * Selects the word at the coordinates specified. Words are defined as all | |
160 | * non-whitespace characters. | |
161 | * @param coords The coordinates to get the word at. | |
162 | */ | |
163 | private _selectWordAt(coords: [number, number]): void { | |
164 | // TODO: Handle double click and drag in both directions! | |
165 | ||
166 | const line = this._translateBufferLineToString(this._buffer.get(coords[1])); | |
167 | // Expand the string in both directions until a space is hit | |
168 | let startCol = coords[0]; | |
169 | let endCol = coords[0]; | |
170 | while (startCol > 0 && line.charAt(startCol - 1) !== ' ') { | |
171 | startCol--; | |
172 | } | |
173 | while (endCol < line.length && line.charAt(endCol) !== ' ') { | |
174 | endCol++; | |
175 | } | |
176 | this._selectionStart = [startCol, coords[1]]; | |
177 | this._selectionEnd = [endCol, coords[1]]; | |
178 | this.refresh(); | |
179 | } | |
70fda994 | 180 | } |