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