Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts
5228 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './nativeEditContext.css';
7
import { isFirefox } from '../../../../../base/browser/browser.js';
8
import { addDisposableListener, getActiveElement, getWindow, getWindowId } from '../../../../../base/browser/dom.js';
9
import { FastDomNode } from '../../../../../base/browser/fastDomNode.js';
10
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
11
import { KeyCode } from '../../../../../base/common/keyCodes.js';
12
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
13
import { EditorOption } from '../../../../common/config/editorOptions.js';
14
import { EndOfLinePreference, IModelDeltaDecoration } from '../../../../common/model.js';
15
import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorationsChangedEvent, ViewFlushedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewZonesChangedEvent } from '../../../../common/viewEvents.js';
16
import { ViewContext } from '../../../../common/viewModel/viewContext.js';
17
import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js';
18
import { ViewController } from '../../../view/viewController.js';
19
import { CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent } from '../clipboardUtils.js';
20
import { AbstractEditContext } from '../editContext.js';
21
import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js';
22
import { ScreenReaderSupport } from './screenReaderSupport.js';
23
import { Range } from '../../../../common/core/range.js';
24
import { Selection } from '../../../../common/core/selection.js';
25
import { Position } from '../../../../common/core/position.js';
26
import { IVisibleRangeProvider } from '../textArea/textAreaEditContext.js';
27
import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js';
28
import { EditContext } from './editContextFactory.js';
29
import { NativeEditContextRegistry } from './nativeEditContextRegistry.js';
30
import { IEditorAriaOptions } from '../../../editorBrowser.js';
31
import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js';
32
import { IME } from '../../../../../base/common/ime.js';
33
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
34
import { ILogService } from '../../../../../platform/log/common/log.js';
35
import { inputLatency } from '../../../../../base/browser/performance.js';
36
import { ViewportData } from '../../../../common/viewLayout/viewLinesViewportData.js';
37
38
// Corresponds to classes in nativeEditContext.css
39
enum CompositionClassName {
40
NONE = 'edit-context-composition-none',
41
SECONDARY = 'edit-context-composition-secondary',
42
PRIMARY = 'edit-context-composition-primary',
43
}
44
45
interface ITextUpdateEvent {
46
text: string;
47
selectionStart: number;
48
selectionEnd: number;
49
updateRangeStart: number;
50
updateRangeEnd: number;
51
}
52
53
export class NativeEditContext extends AbstractEditContext {
54
55
// Text area used to handle paste events
56
public readonly domNode: FastDomNode<HTMLDivElement>;
57
private readonly _imeTextArea: FastDomNode<HTMLTextAreaElement>;
58
private readonly _editContext: EditContext;
59
private readonly _screenReaderSupport: ScreenReaderSupport;
60
private _previousEditContextSelection: OffsetRange = new OffsetRange(0, 0);
61
private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1);
62
63
// Overflow guard container
64
private readonly _parent: HTMLElement;
65
private _parentBounds: DOMRect | null = null;
66
private _decorations: string[] = [];
67
private _primarySelection: Selection = new Selection(1, 1, 1, 1);
68
69
70
private _targetWindowId: number = -1;
71
private _scrollTop: number = 0;
72
private _scrollLeft: number = 0;
73
74
private readonly _focusTracker: FocusTracker;
75
76
constructor(
77
ownerID: string,
78
context: ViewContext,
79
overflowGuardContainer: FastDomNode<HTMLElement>,
80
private readonly _viewController: ViewController,
81
private readonly _visibleRangeProvider: IVisibleRangeProvider,
82
@IInstantiationService instantiationService: IInstantiationService,
83
@ILogService private readonly logService: ILogService
84
) {
85
super(context);
86
87
this.domNode = new FastDomNode(document.createElement('div'));
88
this.domNode.setClassName(`native-edit-context`);
89
this._imeTextArea = new FastDomNode(document.createElement('textarea'));
90
this._imeTextArea.setClassName(`ime-text-area`);
91
this._imeTextArea.setAttribute('readonly', 'true');
92
this._imeTextArea.setAttribute('tabindex', '-1');
93
this._imeTextArea.setAttribute('aria-hidden', 'true');
94
this.domNode.setAttribute('autocorrect', 'off');
95
this.domNode.setAttribute('autocapitalize', 'off');
96
this.domNode.setAttribute('autocomplete', 'off');
97
this.domNode.setAttribute('spellcheck', 'false');
98
99
this._updateDomAttributes();
100
101
overflowGuardContainer.appendChild(this.domNode);
102
overflowGuardContainer.appendChild(this._imeTextArea);
103
this._parent = overflowGuardContainer.domNode;
104
105
this._focusTracker = this._register(new FocusTracker(logService, this.domNode.domNode, (newFocusValue: boolean) => {
106
logService.trace('NativeEditContext#handleFocusChange : ', newFocusValue);
107
this._screenReaderSupport.handleFocusChange(newFocusValue);
108
this._context.viewModel.setHasFocus(newFocusValue);
109
}));
110
111
const window = getWindow(this.domNode.domNode);
112
this._editContext = EditContext.create(window);
113
this.setEditContextOnDomNode();
114
115
this._screenReaderSupport = this._register(instantiationService.createInstance(ScreenReaderSupport, this.domNode, context, this._viewController));
116
117
this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => {
118
this.logService.trace('NativeEditContext#copy');
119
120
// !!!!!
121
// This is a workaround for what we think is an Electron bug where
122
// execCommand('copy') does not always work (it does not fire a clipboard event)
123
// !!!!!
124
// We signal that we have executed a copy command
125
CopyOptions.electronBugWorkaroundCopyEventHasFired = true;
126
127
const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._context, this.logService, isFirefox);
128
this._onWillCopy.fire(copyEvent);
129
if (copyEvent.isHandled) {
130
return;
131
}
132
copyEvent.ensureClipboardGetsEditorData();
133
}));
134
this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => {
135
this.logService.trace('NativeEditContext#cut');
136
const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._context, this.logService, isFirefox);
137
this._onWillCut.fire(cutEvent);
138
if (cutEvent.isHandled) {
139
return;
140
}
141
// Pretend here we touched the text area, as the `cut` event will most likely
142
// result in a `selectionchange` event which we want to ignore
143
this._screenReaderSupport.onWillCut();
144
cutEvent.ensureClipboardGetsEditorData();
145
this.logService.trace('NativeEditContext#cut (before viewController.cut)');
146
this._viewController.cut();
147
}));
148
this._register(addDisposableListener(this.domNode.domNode, 'selectionchange', () => {
149
inputLatency.onSelectionChange();
150
}));
151
152
this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e)));
153
this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e)));
154
this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e)));
155
this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e)));
156
this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => {
157
inputLatency.onBeforeInput();
158
if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') {
159
this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 });
160
}
161
}));
162
this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => {
163
this.logService.trace('NativeEditContext#paste');
164
const pasteEvent = createClipboardPasteEvent(e);
165
this._onWillPaste.fire(pasteEvent);
166
if (pasteEvent.isHandled) {
167
e.preventDefault();
168
return;
169
}
170
e.preventDefault();
171
if (!e.clipboardData) {
172
return;
173
}
174
this.logService.trace('NativeEditContext#paste with id : ', pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length);
175
if (!pasteEvent.text) {
176
return;
177
}
178
let pasteOnNewLine = false;
179
let multicursorText: string[] | null = null;
180
let mode: string | null = null;
181
if (pasteEvent.metadata) {
182
const options = this._context.configuration.options;
183
const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);
184
pasteOnNewLine = emptySelectionClipboard && !!pasteEvent.metadata.isFromEmptySelection;
185
multicursorText = typeof pasteEvent.metadata.multicursorText !== 'undefined' ? pasteEvent.metadata.multicursorText : null;
186
mode = pasteEvent.metadata.mode;
187
}
188
this.logService.trace('NativeEditContext#paste (before viewController.paste)');
189
this._viewController.paste(pasteEvent.text, pasteOnNewLine, multicursorText, mode);
190
}));
191
192
// Edit context events
193
this._register(editContextAddDisposableListener(this._editContext, 'textformatupdate', (e) => this._handleTextFormatUpdate(e)));
194
this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e)));
195
let highSurrogateCharacter: string | undefined;
196
this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => {
197
inputLatency.onInput();
198
const text = e.text;
199
if (text.length === 1) {
200
const charCode = text.charCodeAt(0);
201
if (isHighSurrogate(charCode)) {
202
highSurrogateCharacter = text;
203
return;
204
}
205
if (isLowSurrogate(charCode) && highSurrogateCharacter) {
206
const textUpdateEvent: ITextUpdateEvent = {
207
text: highSurrogateCharacter + text,
208
selectionEnd: e.selectionEnd,
209
selectionStart: e.selectionStart,
210
updateRangeStart: e.updateRangeStart - 1,
211
updateRangeEnd: e.updateRangeEnd - 1
212
};
213
highSurrogateCharacter = undefined;
214
this._emitTypeEvent(this._viewController, textUpdateEvent);
215
return;
216
}
217
}
218
this._emitTypeEvent(this._viewController, e);
219
}));
220
this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => {
221
this._updateEditContext();
222
// Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state
223
// Updates the composition state of the cursor controller which determines behavior of typing with interceptors
224
this._viewController.compositionStart();
225
// Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers
226
this._context.viewModel.onCompositionStart();
227
}));
228
this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => {
229
this._updateEditContext();
230
// Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state
231
// Updates the composition state of the cursor controller which determines behavior of typing with interceptors
232
this._viewController.compositionEnd();
233
// Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers
234
this._context.viewModel.onCompositionEnd();
235
}));
236
let reenableTracking: boolean = false;
237
this._register(IME.onDidChange(() => {
238
if (IME.enabled && reenableTracking) {
239
this._focusTracker.resume();
240
this.domNode.focus();
241
reenableTracking = false;
242
}
243
if (!IME.enabled && this.isFocused()) {
244
this._focusTracker.pause();
245
this._imeTextArea.focus();
246
reenableTracking = true;
247
}
248
}));
249
this._register(NativeEditContextRegistry.register(ownerID, this));
250
}
251
252
// --- Public methods ---
253
254
public override dispose(): void {
255
// Force blue the dom node so can write in pane with no native edit context after disposal
256
this.domNode.domNode.editContext = undefined;
257
this.domNode.domNode.blur();
258
this.domNode.domNode.remove();
259
this._imeTextArea.domNode.remove();
260
super.dispose();
261
}
262
263
public setAriaOptions(options: IEditorAriaOptions): void {
264
this._screenReaderSupport.setAriaOptions(options);
265
}
266
267
/* Last rendered data needed for correct hit-testing and determining the mouse position.
268
* Without this, the selection will blink as incorrect mouse position is calculated */
269
public getLastRenderData(): Position | null {
270
return this._primarySelection.getPosition();
271
}
272
273
public override onBeforeRender(viewportData: ViewportData): void {
274
// We need to read the position of the container dom node
275
// It is best to do this before we begin touching the DOM at all
276
// Because the sync layout will be fast if we do it here
277
this._parentBounds = this._parent.getBoundingClientRect();
278
}
279
280
public override prepareRender(ctx: RenderingContext): void {
281
this._screenReaderSupport.prepareRender(ctx);
282
this._updateSelectionAndControlBoundsData(ctx);
283
}
284
285
public render(ctx: RestrictedRenderingContext): void {
286
this._screenReaderSupport.render(ctx);
287
this._updateSelectionAndControlBounds();
288
}
289
290
public override onCursorStateChanged(e: ViewCursorStateChangedEvent): boolean {
291
this._primarySelection = e.modelSelections[0] ?? new Selection(1, 1, 1, 1);
292
this._screenReaderSupport.onCursorStateChanged(e);
293
this._updateEditContext();
294
return true;
295
}
296
297
public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean {
298
this._screenReaderSupport.onConfigurationChanged(e);
299
this._updateDomAttributes();
300
return true;
301
}
302
303
public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {
304
// true for inline decorations that can end up relayouting text
305
return true;
306
}
307
308
public override onFlushed(e: ViewFlushedEvent): boolean {
309
return true;
310
}
311
312
public override onLinesChanged(e: ViewLinesChangedEvent): boolean {
313
this._updateEditContextOnLineChange(e.fromLineNumber, e.fromLineNumber + e.count - 1);
314
return true;
315
}
316
317
public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {
318
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
319
return true;
320
}
321
322
public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {
323
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
324
return true;
325
}
326
327
private _updateEditContextOnLineChange(fromLineNumber: number, toLineNumber: number): void {
328
if (this._editContextPrimarySelection.endLineNumber < fromLineNumber || this._editContextPrimarySelection.startLineNumber > toLineNumber) {
329
return;
330
}
331
this._updateEditContext();
332
}
333
334
public override onScrollChanged(e: ViewScrollChangedEvent): boolean {
335
this._scrollLeft = e.scrollLeft;
336
this._scrollTop = e.scrollTop;
337
return true;
338
}
339
340
public override onZonesChanged(e: ViewZonesChangedEvent): boolean {
341
return true;
342
}
343
344
public handleWillPaste(): void {
345
this.logService.trace('NativeEditContext#handleWillPaste');
346
this._prepareScreenReaderForPaste();
347
}
348
349
private _prepareScreenReaderForPaste(): void {
350
this._screenReaderSupport.onWillPaste();
351
}
352
353
public handleWillCopy(): void {
354
this.logService.trace('NativeEditContext#handleWillCopy');
355
this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement());
356
}
357
358
public writeScreenReaderContent(): void {
359
this._screenReaderSupport.writeScreenReaderContent();
360
}
361
362
public isFocused(): boolean {
363
return this._focusTracker.isFocused;
364
}
365
366
public focus(): void {
367
this._focusTracker.focus();
368
369
// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus
370
this.refreshFocusState();
371
}
372
373
public refreshFocusState(): void {
374
this._focusTracker.refreshFocusState();
375
}
376
377
// TODO: added as a workaround fix for https://github.com/microsoft/vscode/issues/229825
378
// When this issue will be fixed the following should be removed.
379
public setEditContextOnDomNode(): void {
380
const targetWindow = getWindow(this.domNode.domNode);
381
const targetWindowId = getWindowId(targetWindow);
382
if (this._targetWindowId !== targetWindowId) {
383
this.domNode.domNode.editContext = this._editContext;
384
this._targetWindowId = targetWindowId;
385
}
386
}
387
388
// --- Private methods ---
389
390
private _onKeyUp(e: KeyboardEvent) {
391
inputLatency.onKeyUp();
392
this._viewController.emitKeyUp(new StandardKeyboardEvent(e));
393
}
394
395
private _onKeyDown(e: KeyboardEvent) {
396
inputLatency.onKeyDown();
397
const standardKeyboardEvent = new StandardKeyboardEvent(e);
398
// When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further
399
if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) {
400
standardKeyboardEvent.stopPropagation();
401
}
402
this._viewController.emitKeyDown(standardKeyboardEvent);
403
}
404
405
private _updateDomAttributes(): void {
406
const options = this._context.configuration.options;
407
this.domNode.domNode.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));
408
}
409
410
private _updateEditContext(): void {
411
const editContextState = this._getNewEditContextState();
412
if (!editContextState) {
413
return;
414
}
415
this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text ?? ' ');
416
this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
417
this._editContextPrimarySelection = editContextState.editContextPrimarySelection;
418
this._previousEditContextSelection = new OffsetRange(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
419
}
420
421
private _emitTypeEvent(viewController: ViewController, e: ITextUpdateEvent): void {
422
if (!this._editContext) {
423
return;
424
}
425
const selectionEndOffset = this._previousEditContextSelection.endExclusive;
426
const selectionStartOffset = this._previousEditContextSelection.start;
427
this._previousEditContextSelection = new OffsetRange(e.selectionStart, e.selectionEnd);
428
429
let replaceNextCharCnt = 0;
430
let replacePrevCharCnt = 0;
431
if (e.updateRangeEnd > selectionEndOffset) {
432
replaceNextCharCnt = e.updateRangeEnd - selectionEndOffset;
433
}
434
if (e.updateRangeStart < selectionStartOffset) {
435
replacePrevCharCnt = selectionStartOffset - e.updateRangeStart;
436
}
437
let text = '';
438
if (selectionStartOffset < e.updateRangeStart) {
439
text += this._editContext.text.substring(selectionStartOffset, e.updateRangeStart);
440
}
441
text += e.text;
442
if (selectionEndOffset > e.updateRangeEnd) {
443
text += this._editContext.text.substring(e.updateRangeEnd, selectionEndOffset);
444
}
445
let positionDelta = 0;
446
if (e.selectionStart === e.selectionEnd && selectionStartOffset === selectionEndOffset) {
447
positionDelta = e.selectionStart - (e.updateRangeStart + e.text.length);
448
}
449
const typeInput: ITypeData = {
450
text,
451
replacePrevCharCnt,
452
replaceNextCharCnt,
453
positionDelta
454
};
455
this._onType(viewController, typeInput);
456
}
457
458
private _onType(viewController: ViewController, typeInput: ITypeData): void {
459
if (typeInput.replacePrevCharCnt || typeInput.replaceNextCharCnt || typeInput.positionDelta) {
460
viewController.compositionType(typeInput.text, typeInput.replacePrevCharCnt, typeInput.replaceNextCharCnt, typeInput.positionDelta);
461
} else {
462
viewController.type(typeInput.text);
463
}
464
}
465
466
private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; editContextPrimarySelection: Selection } | undefined {
467
const editContextPrimarySelection = this._primarySelection;
468
const model = this._context.viewModel.model;
469
if (!model.isValidRange(editContextPrimarySelection)) {
470
return;
471
}
472
const primarySelectionStartLine = editContextPrimarySelection.startLineNumber;
473
const primarySelectionEndLine = editContextPrimarySelection.endLineNumber;
474
const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine);
475
const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber);
476
const text = model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined);
477
const selectionStartOffset = editContextPrimarySelection.startColumn - 1;
478
const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber;
479
return {
480
text,
481
selectionStartOffset,
482
selectionEndOffset,
483
editContextPrimarySelection
484
};
485
}
486
487
private _editContextStartPosition(): Position {
488
return new Position(this._editContextPrimarySelection.startLineNumber, 1);
489
}
490
491
private _handleTextFormatUpdate(e: TextFormatUpdateEvent): void {
492
if (!this._editContext) {
493
return;
494
}
495
const formats = e.getTextFormats();
496
const editContextStartPosition = this._editContextStartPosition();
497
const decorations: IModelDeltaDecoration[] = [];
498
formats.forEach(f => {
499
const textModel = this._context.viewModel.model;
500
const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition);
501
const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart);
502
const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd);
503
const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration);
504
const thickness = f.underlineThickness.toLowerCase();
505
let decorationClassName: string = CompositionClassName.NONE;
506
switch (thickness) {
507
case 'thin':
508
decorationClassName = CompositionClassName.SECONDARY;
509
break;
510
case 'thick':
511
decorationClassName = CompositionClassName.PRIMARY;
512
break;
513
}
514
decorations.push({
515
range: decorationRange,
516
options: {
517
description: 'textFormatDecoration',
518
inlineClassName: decorationClassName,
519
}
520
});
521
});
522
this._decorations = this._context.viewModel.model.deltaDecorations(this._decorations, decorations);
523
}
524
525
private _linesVisibleRanges: HorizontalPosition | null = null;
526
private _updateSelectionAndControlBoundsData(ctx: RenderingContext): void {
527
const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);
528
if (this._primarySelection.isEmpty()) {
529
const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition());
530
this._linesVisibleRanges = linesVisibleRanges;
531
} else {
532
this._linesVisibleRanges = null;
533
}
534
}
535
536
private _updateSelectionAndControlBounds() {
537
const options = this._context.configuration.options;
538
const contentLeft = options.get(EditorOption.layoutInfo).contentLeft;
539
540
const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);
541
const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber);
542
const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber);
543
544
// !!! Make sure this doesn't force an extra layout
545
// !!! by using the cached parent bounds read in onBeforeRender
546
const parentBounds = this._parentBounds!;
547
const top = parentBounds.top + verticalOffsetStart - this._scrollTop;
548
const height = verticalOffsetEnd - verticalOffsetStart;
549
let left = parentBounds.left + contentLeft - this._scrollLeft;
550
let width: number;
551
552
if (this._primarySelection.isEmpty()) {
553
if (this._linesVisibleRanges) {
554
left += this._linesVisibleRanges.left;
555
}
556
width = 0;
557
} else {
558
width = parentBounds.width - contentLeft;
559
}
560
561
const selectionBounds = new DOMRect(left, top, width, height);
562
this._editContext.updateSelectionBounds(selectionBounds);
563
this._editContext.updateControlBounds(selectionBounds);
564
}
565
566
private _updateCharacterBounds(e: CharacterBoundsUpdateEvent): void {
567
const options = this._context.configuration.options;
568
const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
569
const contentLeft = options.get(EditorOption.layoutInfo).contentLeft;
570
const parentBounds = this._parentBounds!;
571
572
const characterBounds: DOMRect[] = [];
573
const offsetTransformer = new PositionOffsetTransformer(this._editContext.text);
574
for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) {
575
const editContextStartPosition = offsetTransformer.getPosition(offset);
576
const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1;
577
const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column);
578
const characterEndPosition = characterStartPosition.delta(0, 1);
579
const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition);
580
const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange);
581
const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? [];
582
const lineNumber = characterViewRange.startLineNumber;
583
const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
584
const top = parentBounds.top + characterVerticalOffset - this._scrollTop;
585
586
let left = 0;
587
let width = typicalHalfWidthCharacterWidth;
588
if (characterLinesVisibleRanges.length > 0) {
589
for (const visibleRange of characterLinesVisibleRanges[0].ranges) {
590
left = visibleRange.left;
591
width = visibleRange.width;
592
break;
593
}
594
}
595
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);
596
characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight));
597
}
598
this._editContext.updateCharacterBounds(e.rangeStart, characterBounds);
599
}
600
}
601
602