@xterm/xterm 5.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/css/xterm.css +209 -0
- package/lib/xterm.js +2 -0
- package/lib/xterm.js.map +1 -0
- package/package.json +101 -0
- package/src/browser/AccessibilityManager.ts +278 -0
- package/src/browser/Clipboard.ts +93 -0
- package/src/browser/ColorContrastCache.ts +34 -0
- package/src/browser/Lifecycle.ts +33 -0
- package/src/browser/Linkifier2.ts +416 -0
- package/src/browser/LocalizableStrings.ts +12 -0
- package/src/browser/OscLinkProvider.ts +128 -0
- package/src/browser/RenderDebouncer.ts +83 -0
- package/src/browser/Terminal.ts +1317 -0
- package/src/browser/TimeBasedDebouncer.ts +86 -0
- package/src/browser/Types.d.ts +181 -0
- package/src/browser/Viewport.ts +401 -0
- package/src/browser/decorations/BufferDecorationRenderer.ts +134 -0
- package/src/browser/decorations/ColorZoneStore.ts +117 -0
- package/src/browser/decorations/OverviewRulerRenderer.ts +218 -0
- package/src/browser/input/CompositionHelper.ts +246 -0
- package/src/browser/input/Mouse.ts +54 -0
- package/src/browser/input/MoveToCell.ts +249 -0
- package/src/browser/public/Terminal.ts +260 -0
- package/src/browser/renderer/dom/DomRenderer.ts +509 -0
- package/src/browser/renderer/dom/DomRendererRowFactory.ts +526 -0
- package/src/browser/renderer/dom/WidthCache.ts +160 -0
- package/src/browser/renderer/shared/CellColorResolver.ts +137 -0
- package/src/browser/renderer/shared/CharAtlasCache.ts +96 -0
- package/src/browser/renderer/shared/CharAtlasUtils.ts +75 -0
- package/src/browser/renderer/shared/Constants.ts +14 -0
- package/src/browser/renderer/shared/CursorBlinkStateManager.ts +146 -0
- package/src/browser/renderer/shared/CustomGlyphs.ts +687 -0
- package/src/browser/renderer/shared/DevicePixelObserver.ts +41 -0
- package/src/browser/renderer/shared/README.md +1 -0
- package/src/browser/renderer/shared/RendererUtils.ts +58 -0
- package/src/browser/renderer/shared/SelectionRenderModel.ts +91 -0
- package/src/browser/renderer/shared/TextureAtlas.ts +1082 -0
- package/src/browser/renderer/shared/Types.d.ts +173 -0
- package/src/browser/selection/SelectionModel.ts +144 -0
- package/src/browser/selection/Types.d.ts +15 -0
- package/src/browser/services/CharSizeService.ts +102 -0
- package/src/browser/services/CharacterJoinerService.ts +339 -0
- package/src/browser/services/CoreBrowserService.ts +137 -0
- package/src/browser/services/MouseService.ts +46 -0
- package/src/browser/services/RenderService.ts +279 -0
- package/src/browser/services/SelectionService.ts +1031 -0
- package/src/browser/services/Services.ts +147 -0
- package/src/browser/services/ThemeService.ts +237 -0
- package/src/common/CircularList.ts +241 -0
- package/src/common/Clone.ts +23 -0
- package/src/common/Color.ts +357 -0
- package/src/common/CoreTerminal.ts +284 -0
- package/src/common/EventEmitter.ts +78 -0
- package/src/common/InputHandler.ts +3461 -0
- package/src/common/Lifecycle.ts +108 -0
- package/src/common/MultiKeyMap.ts +42 -0
- package/src/common/Platform.ts +44 -0
- package/src/common/SortedList.ts +118 -0
- package/src/common/TaskQueue.ts +166 -0
- package/src/common/TypedArrayUtils.ts +17 -0
- package/src/common/Types.d.ts +553 -0
- package/src/common/WindowsMode.ts +27 -0
- package/src/common/buffer/AttributeData.ts +196 -0
- package/src/common/buffer/Buffer.ts +654 -0
- package/src/common/buffer/BufferLine.ts +524 -0
- package/src/common/buffer/BufferRange.ts +13 -0
- package/src/common/buffer/BufferReflow.ts +223 -0
- package/src/common/buffer/BufferSet.ts +134 -0
- package/src/common/buffer/CellData.ts +94 -0
- package/src/common/buffer/Constants.ts +149 -0
- package/src/common/buffer/Marker.ts +43 -0
- package/src/common/buffer/Types.d.ts +52 -0
- package/src/common/data/Charsets.ts +256 -0
- package/src/common/data/EscapeSequences.ts +153 -0
- package/src/common/input/Keyboard.ts +398 -0
- package/src/common/input/TextDecoder.ts +346 -0
- package/src/common/input/UnicodeV6.ts +145 -0
- package/src/common/input/WriteBuffer.ts +246 -0
- package/src/common/input/XParseColor.ts +80 -0
- package/src/common/parser/Constants.ts +58 -0
- package/src/common/parser/DcsParser.ts +192 -0
- package/src/common/parser/EscapeSequenceParser.ts +792 -0
- package/src/common/parser/OscParser.ts +238 -0
- package/src/common/parser/Params.ts +229 -0
- package/src/common/parser/Types.d.ts +275 -0
- package/src/common/public/AddonManager.ts +53 -0
- package/src/common/public/BufferApiView.ts +35 -0
- package/src/common/public/BufferLineApiView.ts +29 -0
- package/src/common/public/BufferNamespaceApi.ts +36 -0
- package/src/common/public/ParserApi.ts +37 -0
- package/src/common/public/UnicodeApi.ts +27 -0
- package/src/common/services/BufferService.ts +151 -0
- package/src/common/services/CharsetService.ts +34 -0
- package/src/common/services/CoreMouseService.ts +318 -0
- package/src/common/services/CoreService.ts +87 -0
- package/src/common/services/DecorationService.ts +140 -0
- package/src/common/services/InstantiationService.ts +85 -0
- package/src/common/services/LogService.ts +124 -0
- package/src/common/services/OptionsService.ts +202 -0
- package/src/common/services/OscLinkService.ts +115 -0
- package/src/common/services/ServiceRegistry.ts +49 -0
- package/src/common/services/Services.ts +373 -0
- package/src/common/services/UnicodeService.ts +111 -0
- package/src/headless/Terminal.ts +136 -0
- package/src/headless/public/Terminal.ts +195 -0
- package/typings/xterm.d.ts +1857 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IBufferRange, ILinkifier2 } from 'browser/Types';
|
|
7
|
+
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
|
8
|
+
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
|
9
|
+
import { SelectionModel } from 'browser/selection/SelectionModel';
|
|
10
|
+
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
|
11
|
+
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
|
12
|
+
import { EventEmitter } from 'common/EventEmitter';
|
|
13
|
+
import { Disposable, toDisposable } from 'common/Lifecycle';
|
|
14
|
+
import * as Browser from 'common/Platform';
|
|
15
|
+
import { IBufferLine, IDisposable } from 'common/Types';
|
|
16
|
+
import { getRangeLength } from 'common/buffer/BufferRange';
|
|
17
|
+
import { CellData } from 'common/buffer/CellData';
|
|
18
|
+
import { IBuffer } from 'common/buffer/Types';
|
|
19
|
+
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The number of pixels the mouse needs to be above or below the viewport in
|
|
23
|
+
* order to scroll at the maximum speed.
|
|
24
|
+
*/
|
|
25
|
+
const DRAG_SCROLL_MAX_THRESHOLD = 50;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The maximum scrolling speed
|
|
29
|
+
*/
|
|
30
|
+
const DRAG_SCROLL_MAX_SPEED = 15;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The number of milliseconds between drag scroll updates.
|
|
34
|
+
*/
|
|
35
|
+
const DRAG_SCROLL_INTERVAL = 50;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The maximum amount of time that can have elapsed for an alt click to move the
|
|
39
|
+
* cursor.
|
|
40
|
+
*/
|
|
41
|
+
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
|
42
|
+
|
|
43
|
+
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
|
44
|
+
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Represents a position of a word on a line.
|
|
48
|
+
*/
|
|
49
|
+
interface IWordPosition {
|
|
50
|
+
start: number;
|
|
51
|
+
length: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A selection mode, this drives how the selection behaves on mouse move.
|
|
56
|
+
*/
|
|
57
|
+
export const enum SelectionMode {
|
|
58
|
+
NORMAL,
|
|
59
|
+
WORD,
|
|
60
|
+
LINE,
|
|
61
|
+
COLUMN
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A class that manages the selection of the terminal. With help from
|
|
66
|
+
* SelectionModel, SelectionService handles with all logic associated with
|
|
67
|
+
* dealing with the selection, including handling mouse interaction, wide
|
|
68
|
+
* characters and fetching the actual text within the selection. Rendering is
|
|
69
|
+
* not handled by the SelectionService but the onRedrawRequest event is fired
|
|
70
|
+
* when the selection is ready to be redrawn (on an animation frame).
|
|
71
|
+
*/
|
|
72
|
+
export class SelectionService extends Disposable implements ISelectionService {
|
|
73
|
+
public serviceBrand: undefined;
|
|
74
|
+
|
|
75
|
+
protected _model: SelectionModel;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The amount to scroll every drag scroll update (depends on how far the mouse
|
|
79
|
+
* drag is above or below the terminal).
|
|
80
|
+
*/
|
|
81
|
+
private _dragScrollAmount: number = 0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The current selection mode.
|
|
85
|
+
*/
|
|
86
|
+
protected _activeSelectionMode: SelectionMode;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A setInterval timer that is active while the mouse is down whose callback
|
|
90
|
+
* scrolls the viewport when necessary.
|
|
91
|
+
*/
|
|
92
|
+
private _dragScrollIntervalTimer: number | undefined;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The animation frame ID used for refreshing the selection.
|
|
96
|
+
*/
|
|
97
|
+
private _refreshAnimationFrame: number | undefined;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether selection is enabled.
|
|
101
|
+
*/
|
|
102
|
+
private _enabled = true;
|
|
103
|
+
|
|
104
|
+
private _mouseMoveListener: EventListener;
|
|
105
|
+
private _mouseUpListener: EventListener;
|
|
106
|
+
private _trimListener: IDisposable;
|
|
107
|
+
private _workCell: CellData = new CellData();
|
|
108
|
+
|
|
109
|
+
private _mouseDownTimeStamp: number = 0;
|
|
110
|
+
private _oldHasSelection: boolean = false;
|
|
111
|
+
private _oldSelectionStart: [number, number] | undefined = undefined;
|
|
112
|
+
private _oldSelectionEnd: [number, number] | undefined = undefined;
|
|
113
|
+
|
|
114
|
+
private readonly _onLinuxMouseSelection = this.register(new EventEmitter<string>());
|
|
115
|
+
public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event;
|
|
116
|
+
private readonly _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
|
|
117
|
+
public readonly onRequestRedraw = this._onRedrawRequest.event;
|
|
118
|
+
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
|
|
119
|
+
public readonly onSelectionChange = this._onSelectionChange.event;
|
|
120
|
+
private readonly _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
|
|
121
|
+
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
private readonly _element: HTMLElement,
|
|
125
|
+
private readonly _screenElement: HTMLElement,
|
|
126
|
+
private readonly _linkifier: ILinkifier2,
|
|
127
|
+
@IBufferService private readonly _bufferService: IBufferService,
|
|
128
|
+
@ICoreService private readonly _coreService: ICoreService,
|
|
129
|
+
@IMouseService private readonly _mouseService: IMouseService,
|
|
130
|
+
@IOptionsService private readonly _optionsService: IOptionsService,
|
|
131
|
+
@IRenderService private readonly _renderService: IRenderService,
|
|
132
|
+
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
|
133
|
+
) {
|
|
134
|
+
super();
|
|
135
|
+
|
|
136
|
+
// Init listeners
|
|
137
|
+
this._mouseMoveListener = event => this._handleMouseMove(event as MouseEvent);
|
|
138
|
+
this._mouseUpListener = event => this._handleMouseUp(event as MouseEvent);
|
|
139
|
+
this._coreService.onUserInput(() => {
|
|
140
|
+
if (this.hasSelection) {
|
|
141
|
+
this.clearSelection();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
|
|
145
|
+
this.register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e)));
|
|
146
|
+
|
|
147
|
+
this.enable();
|
|
148
|
+
|
|
149
|
+
this._model = new SelectionModel(this._bufferService);
|
|
150
|
+
this._activeSelectionMode = SelectionMode.NORMAL;
|
|
151
|
+
|
|
152
|
+
this.register(toDisposable(() => {
|
|
153
|
+
this._removeMouseDownListeners();
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public reset(): void {
|
|
158
|
+
this.clearSelection();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Disables the selection manager. This is useful for when terminal mouse
|
|
163
|
+
* are enabled.
|
|
164
|
+
*/
|
|
165
|
+
public disable(): void {
|
|
166
|
+
this.clearSelection();
|
|
167
|
+
this._enabled = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Enable the selection manager.
|
|
172
|
+
*/
|
|
173
|
+
public enable(): void {
|
|
174
|
+
this._enabled = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
|
178
|
+
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Gets whether there is an active text selection.
|
|
182
|
+
*/
|
|
183
|
+
public get hasSelection(): boolean {
|
|
184
|
+
const start = this._model.finalSelectionStart;
|
|
185
|
+
const end = this._model.finalSelectionEnd;
|
|
186
|
+
if (!start || !end) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return start[0] !== end[0] || start[1] !== end[1];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets the text currently selected.
|
|
194
|
+
*/
|
|
195
|
+
public get selectionText(): string {
|
|
196
|
+
const start = this._model.finalSelectionStart;
|
|
197
|
+
const end = this._model.finalSelectionEnd;
|
|
198
|
+
if (!start || !end) {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const buffer = this._bufferService.buffer;
|
|
203
|
+
const result: string[] = [];
|
|
204
|
+
|
|
205
|
+
if (this._activeSelectionMode === SelectionMode.COLUMN) {
|
|
206
|
+
// Ignore zero width selections
|
|
207
|
+
if (start[0] === end[0]) {
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// For column selection it's not enough to rely on final selection's swapping of reversed
|
|
212
|
+
// values, it also needs the x coordinates to swap independently of the y coordinate is needed
|
|
213
|
+
const startCol = start[0] < end[0] ? start[0] : end[0];
|
|
214
|
+
const endCol = start[0] < end[0] ? end[0] : start[0];
|
|
215
|
+
for (let i = start[1]; i <= end[1]; i++) {
|
|
216
|
+
const lineText = buffer.translateBufferLineToString(i, true, startCol, endCol);
|
|
217
|
+
result.push(lineText);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Get first row
|
|
221
|
+
const startRowEndCol = start[1] === end[1] ? end[0] : undefined;
|
|
222
|
+
result.push(buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
|
223
|
+
|
|
224
|
+
// Get middle rows
|
|
225
|
+
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
|
226
|
+
const bufferLine = buffer.lines.get(i);
|
|
227
|
+
const lineText = buffer.translateBufferLineToString(i, true);
|
|
228
|
+
if (bufferLine?.isWrapped) {
|
|
229
|
+
result[result.length - 1] += lineText;
|
|
230
|
+
} else {
|
|
231
|
+
result.push(lineText);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get final row
|
|
236
|
+
if (start[1] !== end[1]) {
|
|
237
|
+
const bufferLine = buffer.lines.get(end[1]);
|
|
238
|
+
const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
|
239
|
+
if (bufferLine && bufferLine!.isWrapped) {
|
|
240
|
+
result[result.length - 1] += lineText;
|
|
241
|
+
} else {
|
|
242
|
+
result.push(lineText);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Format string by replacing non-breaking space chars with regular spaces
|
|
248
|
+
// and joining the array into a multi-line string.
|
|
249
|
+
const formattedResult = result.map(line => {
|
|
250
|
+
return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' ');
|
|
251
|
+
}).join(Browser.isWindows ? '\r\n' : '\n');
|
|
252
|
+
|
|
253
|
+
return formattedResult;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Clears the current terminal selection.
|
|
258
|
+
*/
|
|
259
|
+
public clearSelection(): void {
|
|
260
|
+
this._model.clearSelection();
|
|
261
|
+
this._removeMouseDownListeners();
|
|
262
|
+
this.refresh();
|
|
263
|
+
this._onSelectionChange.fire();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Queues a refresh, redrawing the selection on the next opportunity.
|
|
268
|
+
* @param isLinuxMouseSelection Whether the selection should be registered as a new
|
|
269
|
+
* selection on Linux.
|
|
270
|
+
*/
|
|
271
|
+
public refresh(isLinuxMouseSelection?: boolean): void {
|
|
272
|
+
// Queue the refresh for the renderer
|
|
273
|
+
if (!this._refreshAnimationFrame) {
|
|
274
|
+
this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._refresh());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If the platform is Linux and the refresh call comes from a mouse event,
|
|
278
|
+
// we need to update the selection for middle click to paste selection.
|
|
279
|
+
if (Browser.isLinux && isLinuxMouseSelection) {
|
|
280
|
+
const selectionText = this.selectionText;
|
|
281
|
+
if (selectionText.length) {
|
|
282
|
+
this._onLinuxMouseSelection.fire(this.selectionText);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fires the refresh event, causing consumers to pick it up and redraw the
|
|
289
|
+
* selection state.
|
|
290
|
+
*/
|
|
291
|
+
private _refresh(): void {
|
|
292
|
+
this._refreshAnimationFrame = undefined;
|
|
293
|
+
this._onRedrawRequest.fire({
|
|
294
|
+
start: this._model.finalSelectionStart,
|
|
295
|
+
end: this._model.finalSelectionEnd,
|
|
296
|
+
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Checks if the current click was inside the current selection
|
|
302
|
+
* @param event The mouse event
|
|
303
|
+
*/
|
|
304
|
+
private _isClickInSelection(event: MouseEvent): boolean {
|
|
305
|
+
const coords = this._getMouseBufferCoords(event);
|
|
306
|
+
const start = this._model.finalSelectionStart;
|
|
307
|
+
const end = this._model.finalSelectionEnd;
|
|
308
|
+
|
|
309
|
+
if (!start || !end || !coords) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return this._areCoordsInSelection(coords, start, end);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public isCellInSelection(x: number, y: number): boolean {
|
|
317
|
+
const start = this._model.finalSelectionStart;
|
|
318
|
+
const end = this._model.finalSelectionEnd;
|
|
319
|
+
if (!start || !end) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return this._areCoordsInSelection([x, y], start, end);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
|
|
326
|
+
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
|
327
|
+
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
|
|
328
|
+
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) ||
|
|
329
|
+
(start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Selects word at the current mouse event coordinates.
|
|
334
|
+
* @param event The mouse event.
|
|
335
|
+
*/
|
|
336
|
+
private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean {
|
|
337
|
+
// Check if there is a link under the cursor first and select that if so
|
|
338
|
+
const range = this._linkifier.currentLink?.link?.range;
|
|
339
|
+
if (range) {
|
|
340
|
+
this._model.selectionStart = [range.start.x - 1, range.start.y - 1];
|
|
341
|
+
this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols);
|
|
342
|
+
this._model.selectionEnd = undefined;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const coords = this._getMouseBufferCoords(event);
|
|
347
|
+
if (coords) {
|
|
348
|
+
this._selectWordAt(coords, allowWhitespaceOnlySelection);
|
|
349
|
+
this._model.selectionEnd = undefined;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Selects all text within the terminal.
|
|
357
|
+
*/
|
|
358
|
+
public selectAll(): void {
|
|
359
|
+
this._model.isSelectAllActive = true;
|
|
360
|
+
this.refresh();
|
|
361
|
+
this._onSelectionChange.fire();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public selectLines(start: number, end: number): void {
|
|
365
|
+
this._model.clearSelection();
|
|
366
|
+
start = Math.max(start, 0);
|
|
367
|
+
end = Math.min(end, this._bufferService.buffer.lines.length - 1);
|
|
368
|
+
this._model.selectionStart = [0, start];
|
|
369
|
+
this._model.selectionEnd = [this._bufferService.cols, end];
|
|
370
|
+
this.refresh();
|
|
371
|
+
this._onSelectionChange.fire();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Handle the buffer being trimmed, adjust the selection position.
|
|
376
|
+
* @param amount The amount the buffer is being trimmed.
|
|
377
|
+
*/
|
|
378
|
+
private _handleTrim(amount: number): void {
|
|
379
|
+
const needsRefresh = this._model.handleTrim(amount);
|
|
380
|
+
if (needsRefresh) {
|
|
381
|
+
this.refresh();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Gets the 0-based [x, y] buffer coordinates of the current mouse event.
|
|
387
|
+
* @param event The mouse event.
|
|
388
|
+
*/
|
|
389
|
+
private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
|
|
390
|
+
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
|
391
|
+
if (!coords) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Convert to 0-based
|
|
396
|
+
coords[0]--;
|
|
397
|
+
coords[1]--;
|
|
398
|
+
|
|
399
|
+
// Convert viewport coords to buffer coords
|
|
400
|
+
coords[1] += this._bufferService.buffer.ydisp;
|
|
401
|
+
return coords;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Gets the amount the viewport should be scrolled based on how far out of the
|
|
406
|
+
* terminal the mouse is.
|
|
407
|
+
* @param event The mouse event.
|
|
408
|
+
*/
|
|
409
|
+
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
|
410
|
+
let offset = getCoordsRelativeToElement(this._coreBrowserService.window, event, this._screenElement)[1];
|
|
411
|
+
const terminalHeight = this._renderService.dimensions.css.canvas.height;
|
|
412
|
+
if (offset >= 0 && offset <= terminalHeight) {
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
if (offset > terminalHeight) {
|
|
416
|
+
offset -= terminalHeight;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
|
|
420
|
+
offset /= DRAG_SCROLL_MAX_THRESHOLD;
|
|
421
|
+
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Returns whether the selection manager should force selection, regardless of
|
|
426
|
+
* whether the terminal is in mouse events mode.
|
|
427
|
+
* @param event The mouse event.
|
|
428
|
+
*/
|
|
429
|
+
public shouldForceSelection(event: MouseEvent): boolean {
|
|
430
|
+
if (Browser.isMac) {
|
|
431
|
+
return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return event.shiftKey;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Handles te mousedown event, setting up for a new selection.
|
|
439
|
+
* @param event The mousedown event.
|
|
440
|
+
*/
|
|
441
|
+
public handleMouseDown(event: MouseEvent): void {
|
|
442
|
+
this._mouseDownTimeStamp = event.timeStamp;
|
|
443
|
+
// If we have selection, we want the context menu on right click even if the
|
|
444
|
+
// terminal is in mouse mode.
|
|
445
|
+
if (event.button === 2 && this.hasSelection) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Only action the primary button
|
|
450
|
+
if (event.button !== 0) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Allow selection when using a specific modifier key, even when disabled
|
|
455
|
+
if (!this._enabled) {
|
|
456
|
+
if (!this.shouldForceSelection(event)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Don't send the mouse down event to the current process, we want to select
|
|
461
|
+
event.stopPropagation();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Tell the browser not to start a regular selection
|
|
465
|
+
event.preventDefault();
|
|
466
|
+
|
|
467
|
+
// Reset drag scroll state
|
|
468
|
+
this._dragScrollAmount = 0;
|
|
469
|
+
|
|
470
|
+
if (this._enabled && event.shiftKey) {
|
|
471
|
+
this._handleIncrementalClick(event);
|
|
472
|
+
} else {
|
|
473
|
+
if (event.detail === 1) {
|
|
474
|
+
this._handleSingleClick(event);
|
|
475
|
+
} else if (event.detail === 2) {
|
|
476
|
+
this._handleDoubleClick(event);
|
|
477
|
+
} else if (event.detail === 3) {
|
|
478
|
+
this._handleTripleClick(event);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this._addMouseDownListeners();
|
|
483
|
+
this.refresh(true);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Adds listeners when mousedown is triggered.
|
|
488
|
+
*/
|
|
489
|
+
private _addMouseDownListeners(): void {
|
|
490
|
+
// Listen on the document so that dragging outside of viewport works
|
|
491
|
+
if (this._screenElement.ownerDocument) {
|
|
492
|
+
this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
|
493
|
+
this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
|
494
|
+
}
|
|
495
|
+
this._dragScrollIntervalTimer = this._coreBrowserService.window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Removes the listeners that are registered when mousedown is triggered.
|
|
500
|
+
*/
|
|
501
|
+
private _removeMouseDownListeners(): void {
|
|
502
|
+
if (this._screenElement.ownerDocument) {
|
|
503
|
+
this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
|
504
|
+
this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
|
505
|
+
}
|
|
506
|
+
this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer);
|
|
507
|
+
this._dragScrollIntervalTimer = undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Performs an incremental click, setting the selection end position to the mouse
|
|
512
|
+
* position.
|
|
513
|
+
* @param event The mouse event.
|
|
514
|
+
*/
|
|
515
|
+
private _handleIncrementalClick(event: MouseEvent): void {
|
|
516
|
+
if (this._model.selectionStart) {
|
|
517
|
+
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Performs a single click, resetting relevant state and setting the selection
|
|
523
|
+
* start position.
|
|
524
|
+
* @param event The mouse event.
|
|
525
|
+
*/
|
|
526
|
+
private _handleSingleClick(event: MouseEvent): void {
|
|
527
|
+
this._model.selectionStartLength = 0;
|
|
528
|
+
this._model.isSelectAllActive = false;
|
|
529
|
+
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
|
|
530
|
+
|
|
531
|
+
// Initialize the new selection
|
|
532
|
+
this._model.selectionStart = this._getMouseBufferCoords(event);
|
|
533
|
+
if (!this._model.selectionStart) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this._model.selectionEnd = undefined;
|
|
537
|
+
|
|
538
|
+
// Ensure the line exists
|
|
539
|
+
const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]);
|
|
540
|
+
if (!line) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Return early if the click event is not in the buffer (eg. in scroll bar)
|
|
545
|
+
if (line.length === this._model.selectionStart[0]) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// If the mouse is over the second half of a wide character, adjust the
|
|
550
|
+
// selection to cover the whole character
|
|
551
|
+
if (line.hasWidth(this._model.selectionStart[0]) === 0) {
|
|
552
|
+
this._model.selectionStart[0]++;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Performs a double click, selecting the current word.
|
|
558
|
+
* @param event The mouse event.
|
|
559
|
+
*/
|
|
560
|
+
private _handleDoubleClick(event: MouseEvent): void {
|
|
561
|
+
if (this._selectWordAtCursor(event, true)) {
|
|
562
|
+
this._activeSelectionMode = SelectionMode.WORD;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Performs a triple click, selecting the current line and activating line
|
|
568
|
+
* select mode.
|
|
569
|
+
* @param event The mouse event.
|
|
570
|
+
*/
|
|
571
|
+
private _handleTripleClick(event: MouseEvent): void {
|
|
572
|
+
const coords = this._getMouseBufferCoords(event);
|
|
573
|
+
if (coords) {
|
|
574
|
+
this._activeSelectionMode = SelectionMode.LINE;
|
|
575
|
+
this._selectLineAt(coords[1]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Returns whether the selection manager should operate in column select mode
|
|
581
|
+
* @param event the mouse or keyboard event
|
|
582
|
+
*/
|
|
583
|
+
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
|
|
584
|
+
return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Handles the mousemove event when the mouse button is down, recording the
|
|
589
|
+
* end of the selection and refreshing the selection.
|
|
590
|
+
* @param event The mousemove event.
|
|
591
|
+
*/
|
|
592
|
+
private _handleMouseMove(event: MouseEvent): void {
|
|
593
|
+
// If the mousemove listener is active it means that a selection is
|
|
594
|
+
// currently being made, we should stop propagation to prevent mouse events
|
|
595
|
+
// to be sent to the pty.
|
|
596
|
+
event.stopImmediatePropagation();
|
|
597
|
+
|
|
598
|
+
// Do nothing if there is no selection start, this can happen if the first
|
|
599
|
+
// click in the terminal is an incremental click
|
|
600
|
+
if (!this._model.selectionStart) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Record the previous position so we know whether to redraw the selection
|
|
605
|
+
// at the end.
|
|
606
|
+
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
|
607
|
+
|
|
608
|
+
// Set the initial selection end based on the mouse coordinates
|
|
609
|
+
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
|
610
|
+
if (!this._model.selectionEnd) {
|
|
611
|
+
this.refresh(true);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Select the entire line if line select mode is active.
|
|
616
|
+
if (this._activeSelectionMode === SelectionMode.LINE) {
|
|
617
|
+
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
|
|
618
|
+
this._model.selectionEnd[0] = 0;
|
|
619
|
+
} else {
|
|
620
|
+
this._model.selectionEnd[0] = this._bufferService.cols;
|
|
621
|
+
}
|
|
622
|
+
} else if (this._activeSelectionMode === SelectionMode.WORD) {
|
|
623
|
+
this._selectToWordAt(this._model.selectionEnd);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Determine the amount of scrolling that will happen.
|
|
627
|
+
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
|
628
|
+
|
|
629
|
+
// If the cursor was above or below the viewport, make sure it's at the
|
|
630
|
+
// start or end of the viewport respectively. This should only happen when
|
|
631
|
+
// NOT in column select mode.
|
|
632
|
+
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
|
633
|
+
if (this._dragScrollAmount > 0) {
|
|
634
|
+
this._model.selectionEnd[0] = this._bufferService.cols;
|
|
635
|
+
} else if (this._dragScrollAmount < 0) {
|
|
636
|
+
this._model.selectionEnd[0] = 0;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// If the character is a wide character include the cell to the right in the
|
|
641
|
+
// selection. Note that selections at the very end of the line will never
|
|
642
|
+
// have a character.
|
|
643
|
+
const buffer = this._bufferService.buffer;
|
|
644
|
+
if (this._model.selectionEnd[1] < buffer.lines.length) {
|
|
645
|
+
const line = buffer.lines.get(this._model.selectionEnd[1]);
|
|
646
|
+
if (line && line.hasWidth(this._model.selectionEnd[0]) === 0) {
|
|
647
|
+
if (this._model.selectionEnd[0] < this._bufferService.cols) {
|
|
648
|
+
this._model.selectionEnd[0]++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Only draw here if the selection changes.
|
|
654
|
+
if (!previousSelectionEnd ||
|
|
655
|
+
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
|
|
656
|
+
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
|
|
657
|
+
this.refresh(true);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
|
|
663
|
+
* scrolling of the viewport.
|
|
664
|
+
*/
|
|
665
|
+
private _dragScroll(): void {
|
|
666
|
+
if (!this._model.selectionEnd || !this._model.selectionStart) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (this._dragScrollAmount) {
|
|
670
|
+
this._onRequestScrollLines.fire({ amount: this._dragScrollAmount, suppressScrollEvent: false });
|
|
671
|
+
// Re-evaluate selection
|
|
672
|
+
// If the cursor was above or below the viewport, make sure it's at the
|
|
673
|
+
// start or end of the viewport respectively. This should only happen when
|
|
674
|
+
// NOT in column select mode.
|
|
675
|
+
const buffer = this._bufferService.buffer;
|
|
676
|
+
if (this._dragScrollAmount > 0) {
|
|
677
|
+
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
|
678
|
+
this._model.selectionEnd[0] = this._bufferService.cols;
|
|
679
|
+
}
|
|
680
|
+
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1);
|
|
681
|
+
} else {
|
|
682
|
+
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
|
683
|
+
this._model.selectionEnd[0] = 0;
|
|
684
|
+
}
|
|
685
|
+
this._model.selectionEnd[1] = buffer.ydisp;
|
|
686
|
+
}
|
|
687
|
+
this.refresh();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Handles the mouseup event, removing the mousedown listeners.
|
|
693
|
+
* @param event The mouseup event.
|
|
694
|
+
*/
|
|
695
|
+
private _handleMouseUp(event: MouseEvent): void {
|
|
696
|
+
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
|
697
|
+
|
|
698
|
+
this._removeMouseDownListeners();
|
|
699
|
+
|
|
700
|
+
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
|
701
|
+
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
|
|
702
|
+
const coordinates = this._mouseService.getCoords(
|
|
703
|
+
event,
|
|
704
|
+
this._element,
|
|
705
|
+
this._bufferService.cols,
|
|
706
|
+
this._bufferService.rows,
|
|
707
|
+
false
|
|
708
|
+
);
|
|
709
|
+
if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) {
|
|
710
|
+
const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._coreService.decPrivateModes.applicationCursorKeys);
|
|
711
|
+
this._coreService.triggerDataEvent(sequence, true);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
this._fireEventIfSelectionChanged();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private _fireEventIfSelectionChanged(): void {
|
|
720
|
+
const start = this._model.finalSelectionStart;
|
|
721
|
+
const end = this._model.finalSelectionEnd;
|
|
722
|
+
const hasSelection = !!start && !!end && (start[0] !== end[0] || start[1] !== end[1]);
|
|
723
|
+
|
|
724
|
+
if (!hasSelection) {
|
|
725
|
+
if (this._oldHasSelection) {
|
|
726
|
+
this._fireOnSelectionChange(start, end, hasSelection);
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Sanity check, these should not be undefined as there is a selection
|
|
732
|
+
if (!start || !end) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (!this._oldSelectionStart || !this._oldSelectionEnd || (
|
|
737
|
+
start[0] !== this._oldSelectionStart[0] || start[1] !== this._oldSelectionStart[1] ||
|
|
738
|
+
end[0] !== this._oldSelectionEnd[0] || end[1] !== this._oldSelectionEnd[1])) {
|
|
739
|
+
|
|
740
|
+
this._fireOnSelectionChange(start, end, hasSelection);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private _fireOnSelectionChange(start: [number, number] | undefined, end: [number, number] | undefined, hasSelection: boolean): void {
|
|
745
|
+
this._oldSelectionStart = start;
|
|
746
|
+
this._oldSelectionEnd = end;
|
|
747
|
+
this._oldHasSelection = hasSelection;
|
|
748
|
+
this._onSelectionChange.fire();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private _handleBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
|
|
752
|
+
this.clearSelection();
|
|
753
|
+
// Only adjust the selection on trim, shiftElements is rarely used (only in
|
|
754
|
+
// reverseIndex) and delete in a splice is only ever used when the same
|
|
755
|
+
// number of elements was just added. Given this is could actually be
|
|
756
|
+
// beneficial to leave the selection as is for these cases.
|
|
757
|
+
this._trimListener.dispose();
|
|
758
|
+
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Converts a viewport column (0 to cols - 1) to the character index on the
|
|
763
|
+
* buffer line, the latter takes into account wide and null characters.
|
|
764
|
+
* @param bufferLine The buffer line to use.
|
|
765
|
+
* @param x The x index in the buffer line to convert.
|
|
766
|
+
*/
|
|
767
|
+
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, x: number): number {
|
|
768
|
+
let charIndex = x;
|
|
769
|
+
for (let i = 0; x >= i; i++) {
|
|
770
|
+
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
|
|
771
|
+
if (this._workCell.getWidth() === 0) {
|
|
772
|
+
// Wide characters aren't included in the line string so decrement the
|
|
773
|
+
// index so the index is back on the wide character.
|
|
774
|
+
charIndex--;
|
|
775
|
+
} else if (length > 1 && x !== i) {
|
|
776
|
+
// Emojis take up multiple characters, so adjust accordingly. For these
|
|
777
|
+
// we don't want ot include the character at the column as we're
|
|
778
|
+
// returning the start index in the string, not the end index.
|
|
779
|
+
charIndex += length - 1;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return charIndex;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
public setSelection(col: number, row: number, length: number): void {
|
|
786
|
+
this._model.clearSelection();
|
|
787
|
+
this._removeMouseDownListeners();
|
|
788
|
+
this._model.selectionStart = [col, row];
|
|
789
|
+
this._model.selectionStartLength = length;
|
|
790
|
+
this.refresh();
|
|
791
|
+
this._fireEventIfSelectionChanged();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
public rightClickSelect(ev: MouseEvent): void {
|
|
795
|
+
if (!this._isClickInSelection(ev)) {
|
|
796
|
+
if (this._selectWordAtCursor(ev, false)) {
|
|
797
|
+
this.refresh(true);
|
|
798
|
+
}
|
|
799
|
+
this._fireEventIfSelectionChanged();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Gets positional information for the word at the coordinated specified.
|
|
805
|
+
* @param coords The coordinates to get the word at.
|
|
806
|
+
*/
|
|
807
|
+
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition | undefined {
|
|
808
|
+
// Ensure coords are within viewport (eg. not within scroll bar)
|
|
809
|
+
if (coords[0] >= this._bufferService.cols) {
|
|
810
|
+
return undefined;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const buffer = this._bufferService.buffer;
|
|
814
|
+
const bufferLine = buffer.lines.get(coords[1]);
|
|
815
|
+
if (!bufferLine) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const line = buffer.translateBufferLineToString(coords[1], false);
|
|
820
|
+
|
|
821
|
+
// Get actual index, taking into consideration wide characters
|
|
822
|
+
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords[0]);
|
|
823
|
+
let endIndex = startIndex;
|
|
824
|
+
|
|
825
|
+
// Record offset to be used later
|
|
826
|
+
const charOffset = coords[0] - startIndex;
|
|
827
|
+
let leftWideCharCount = 0;
|
|
828
|
+
let rightWideCharCount = 0;
|
|
829
|
+
let leftLongCharOffset = 0;
|
|
830
|
+
let rightLongCharOffset = 0;
|
|
831
|
+
|
|
832
|
+
if (line.charAt(startIndex) === ' ') {
|
|
833
|
+
// Expand until non-whitespace is hit
|
|
834
|
+
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
|
|
835
|
+
startIndex--;
|
|
836
|
+
}
|
|
837
|
+
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
|
|
838
|
+
endIndex++;
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
// Expand until whitespace is hit. This algorithm works by scanning left
|
|
842
|
+
// and right from the starting position, keeping both the index format
|
|
843
|
+
// (line) and the column format (bufferLine) in sync. When a wide
|
|
844
|
+
// character is hit, it is recorded and the column index is adjusted.
|
|
845
|
+
let startCol = coords[0];
|
|
846
|
+
let endCol = coords[0];
|
|
847
|
+
|
|
848
|
+
// Consider the initial position, skip it and increment the wide char
|
|
849
|
+
// variable
|
|
850
|
+
if (bufferLine.getWidth(startCol) === 0) {
|
|
851
|
+
leftWideCharCount++;
|
|
852
|
+
startCol--;
|
|
853
|
+
}
|
|
854
|
+
if (bufferLine.getWidth(endCol) === 2) {
|
|
855
|
+
rightWideCharCount++;
|
|
856
|
+
endCol++;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Adjust the end index for characters whose length are > 1 (emojis)
|
|
860
|
+
const length = bufferLine.getString(endCol).length;
|
|
861
|
+
if (length > 1) {
|
|
862
|
+
rightLongCharOffset += length - 1;
|
|
863
|
+
endIndex += length - 1;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Expand the string in both directions until a space is hit
|
|
867
|
+
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
|
|
868
|
+
bufferLine.loadCell(startCol - 1, this._workCell);
|
|
869
|
+
const length = this._workCell.getChars().length;
|
|
870
|
+
if (this._workCell.getWidth() === 0) {
|
|
871
|
+
// If the next character is a wide char, record it and skip the column
|
|
872
|
+
leftWideCharCount++;
|
|
873
|
+
startCol--;
|
|
874
|
+
} else if (length > 1) {
|
|
875
|
+
// If the next character's string is longer than 1 char (eg. emoji),
|
|
876
|
+
// adjust the index
|
|
877
|
+
leftLongCharOffset += length - 1;
|
|
878
|
+
startIndex -= length - 1;
|
|
879
|
+
}
|
|
880
|
+
startIndex--;
|
|
881
|
+
startCol--;
|
|
882
|
+
}
|
|
883
|
+
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
|
|
884
|
+
bufferLine.loadCell(endCol + 1, this._workCell);
|
|
885
|
+
const length = this._workCell.getChars().length;
|
|
886
|
+
if (this._workCell.getWidth() === 2) {
|
|
887
|
+
// If the next character is a wide char, record it and skip the column
|
|
888
|
+
rightWideCharCount++;
|
|
889
|
+
endCol++;
|
|
890
|
+
} else if (length > 1) {
|
|
891
|
+
// If the next character's string is longer than 1 char (eg. emoji),
|
|
892
|
+
// adjust the index
|
|
893
|
+
rightLongCharOffset += length - 1;
|
|
894
|
+
endIndex += length - 1;
|
|
895
|
+
}
|
|
896
|
+
endIndex++;
|
|
897
|
+
endCol++;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Incremenet the end index so it is at the start of the next character
|
|
902
|
+
endIndex++;
|
|
903
|
+
|
|
904
|
+
// Calculate the start _column_, converting the the string indexes back to
|
|
905
|
+
// column coordinates.
|
|
906
|
+
let start =
|
|
907
|
+
startIndex // The index of the selection's start char in the line string
|
|
908
|
+
+ charOffset // The difference between the initial char's column and index
|
|
909
|
+
- leftWideCharCount // The number of wide chars left of the initial char
|
|
910
|
+
+ leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
|
911
|
+
|
|
912
|
+
// Calculate the length in _columns_, converting the the string indexes back
|
|
913
|
+
// to column coordinates.
|
|
914
|
+
let length = Math.min(this._bufferService.cols, // Disallow lengths larger than the terminal cols
|
|
915
|
+
endIndex // The index of the selection's end char in the line string
|
|
916
|
+
- startIndex // The index of the selection's start char in the line string
|
|
917
|
+
+ leftWideCharCount // The number of wide chars left of the initial char
|
|
918
|
+
+ rightWideCharCount // The number of wide chars right of the initial char (inclusive)
|
|
919
|
+
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
|
920
|
+
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
|
|
921
|
+
|
|
922
|
+
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
|
927
|
+
if (followWrappedLinesAbove) {
|
|
928
|
+
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /* ' ' */) {
|
|
929
|
+
const previousBufferLine = buffer.lines.get(coords[1] - 1);
|
|
930
|
+
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /* ' ' */) {
|
|
931
|
+
const previousLineWordPosition = this._getWordAt([this._bufferService.cols - 1, coords[1] - 1], false, true, false);
|
|
932
|
+
if (previousLineWordPosition) {
|
|
933
|
+
const offset = this._bufferService.cols - previousLineWordPosition.start;
|
|
934
|
+
start -= offset;
|
|
935
|
+
length += offset;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
|
942
|
+
if (followWrappedLinesBelow) {
|
|
943
|
+
if (start + length === this._bufferService.cols && bufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /* ' ' */) {
|
|
944
|
+
const nextBufferLine = buffer.lines.get(coords[1] + 1);
|
|
945
|
+
if (nextBufferLine?.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /* ' ' */) {
|
|
946
|
+
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
|
947
|
+
if (nextLineWordPosition) {
|
|
948
|
+
length += nextLineWordPosition.length;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return { start, length };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Selects the word at the coordinates specified.
|
|
959
|
+
* @param coords The coordinates to get the word at.
|
|
960
|
+
* @param allowWhitespaceOnlySelection If whitespace should be selected
|
|
961
|
+
*/
|
|
962
|
+
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
|
963
|
+
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
|
964
|
+
if (wordPosition) {
|
|
965
|
+
// Adjust negative start value
|
|
966
|
+
while (wordPosition.start < 0) {
|
|
967
|
+
wordPosition.start += this._bufferService.cols;
|
|
968
|
+
coords[1]--;
|
|
969
|
+
}
|
|
970
|
+
this._model.selectionStart = [wordPosition.start, coords[1]];
|
|
971
|
+
this._model.selectionStartLength = wordPosition.length;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Sets the selection end to the word at the coordinated specified.
|
|
977
|
+
* @param coords The coordinates to get the word at.
|
|
978
|
+
*/
|
|
979
|
+
private _selectToWordAt(coords: [number, number]): void {
|
|
980
|
+
const wordPosition = this._getWordAt(coords, true);
|
|
981
|
+
if (wordPosition) {
|
|
982
|
+
let endRow = coords[1];
|
|
983
|
+
|
|
984
|
+
// Adjust negative start value
|
|
985
|
+
while (wordPosition.start < 0) {
|
|
986
|
+
wordPosition.start += this._bufferService.cols;
|
|
987
|
+
endRow--;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
|
991
|
+
// case we're interested in the start of the word, not the end
|
|
992
|
+
if (!this._model.areSelectionValuesReversed()) {
|
|
993
|
+
while (wordPosition.start + wordPosition.length > this._bufferService.cols) {
|
|
994
|
+
wordPosition.length -= this._bufferService.cols;
|
|
995
|
+
endRow++;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Gets whether the character is considered a word separator by the select
|
|
1005
|
+
* word logic.
|
|
1006
|
+
* @param cell The cell to check.
|
|
1007
|
+
*/
|
|
1008
|
+
private _isCharWordSeparator(cell: CellData): boolean {
|
|
1009
|
+
// Zero width characters are never separators as they are always to the
|
|
1010
|
+
// right of wide characters
|
|
1011
|
+
if (cell.getWidth() === 0) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
return this._optionsService.rawOptions.wordSeparator.indexOf(cell.getChars()) >= 0;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Selects the line specified.
|
|
1019
|
+
* @param line The line index.
|
|
1020
|
+
*/
|
|
1021
|
+
protected _selectLineAt(line: number): void {
|
|
1022
|
+
const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line);
|
|
1023
|
+
const range: IBufferRange = {
|
|
1024
|
+
start: { x: 0, y: wrappedRange.first },
|
|
1025
|
+
end: { x: this._bufferService.cols - 1, y: wrappedRange.last }
|
|
1026
|
+
};
|
|
1027
|
+
this._model.selectionStart = [0, wrappedRange.first];
|
|
1028
|
+
this._model.selectionEnd = undefined;
|
|
1029
|
+
this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols);
|
|
1030
|
+
}
|
|
1031
|
+
}
|