Path: blob/main/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts
5236 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './nativeEditContext.css';6import { isFirefox } from '../../../../../base/browser/browser.js';7import { addDisposableListener, getActiveElement, getWindow, getWindowId } from '../../../../../base/browser/dom.js';8import { FastDomNode } from '../../../../../base/browser/fastDomNode.js';9import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';10import { KeyCode } from '../../../../../base/common/keyCodes.js';11import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';12import { EditorOption } from '../../../../common/config/editorOptions.js';13import { EndOfLinePreference, IModelDeltaDecoration } from '../../../../common/model.js';14import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorationsChangedEvent, ViewFlushedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewZonesChangedEvent } from '../../../../common/viewEvents.js';15import { ViewContext } from '../../../../common/viewModel/viewContext.js';16import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js';17import { ViewController } from '../../../view/viewController.js';18import { CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent } from '../clipboardUtils.js';19import { AbstractEditContext } from '../editContext.js';20import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js';21import { ScreenReaderSupport } from './screenReaderSupport.js';22import { Range } from '../../../../common/core/range.js';23import { Selection } from '../../../../common/core/selection.js';24import { Position } from '../../../../common/core/position.js';25import { IVisibleRangeProvider } from '../textArea/textAreaEditContext.js';26import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js';27import { EditContext } from './editContextFactory.js';28import { NativeEditContextRegistry } from './nativeEditContextRegistry.js';29import { IEditorAriaOptions } from '../../../editorBrowser.js';30import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js';31import { IME } from '../../../../../base/common/ime.js';32import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';33import { ILogService } from '../../../../../platform/log/common/log.js';34import { inputLatency } from '../../../../../base/browser/performance.js';35import { ViewportData } from '../../../../common/viewLayout/viewLinesViewportData.js';3637// Corresponds to classes in nativeEditContext.css38enum CompositionClassName {39NONE = 'edit-context-composition-none',40SECONDARY = 'edit-context-composition-secondary',41PRIMARY = 'edit-context-composition-primary',42}4344interface ITextUpdateEvent {45text: string;46selectionStart: number;47selectionEnd: number;48updateRangeStart: number;49updateRangeEnd: number;50}5152export class NativeEditContext extends AbstractEditContext {5354// Text area used to handle paste events55public readonly domNode: FastDomNode<HTMLDivElement>;56private readonly _imeTextArea: FastDomNode<HTMLTextAreaElement>;57private readonly _editContext: EditContext;58private readonly _screenReaderSupport: ScreenReaderSupport;59private _previousEditContextSelection: OffsetRange = new OffsetRange(0, 0);60private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1);6162// Overflow guard container63private readonly _parent: HTMLElement;64private _parentBounds: DOMRect | null = null;65private _decorations: string[] = [];66private _primarySelection: Selection = new Selection(1, 1, 1, 1);676869private _targetWindowId: number = -1;70private _scrollTop: number = 0;71private _scrollLeft: number = 0;7273private readonly _focusTracker: FocusTracker;7475constructor(76ownerID: string,77context: ViewContext,78overflowGuardContainer: FastDomNode<HTMLElement>,79private readonly _viewController: ViewController,80private readonly _visibleRangeProvider: IVisibleRangeProvider,81@IInstantiationService instantiationService: IInstantiationService,82@ILogService private readonly logService: ILogService83) {84super(context);8586this.domNode = new FastDomNode(document.createElement('div'));87this.domNode.setClassName(`native-edit-context`);88this._imeTextArea = new FastDomNode(document.createElement('textarea'));89this._imeTextArea.setClassName(`ime-text-area`);90this._imeTextArea.setAttribute('readonly', 'true');91this._imeTextArea.setAttribute('tabindex', '-1');92this._imeTextArea.setAttribute('aria-hidden', 'true');93this.domNode.setAttribute('autocorrect', 'off');94this.domNode.setAttribute('autocapitalize', 'off');95this.domNode.setAttribute('autocomplete', 'off');96this.domNode.setAttribute('spellcheck', 'false');9798this._updateDomAttributes();99100overflowGuardContainer.appendChild(this.domNode);101overflowGuardContainer.appendChild(this._imeTextArea);102this._parent = overflowGuardContainer.domNode;103104this._focusTracker = this._register(new FocusTracker(logService, this.domNode.domNode, (newFocusValue: boolean) => {105logService.trace('NativeEditContext#handleFocusChange : ', newFocusValue);106this._screenReaderSupport.handleFocusChange(newFocusValue);107this._context.viewModel.setHasFocus(newFocusValue);108}));109110const window = getWindow(this.domNode.domNode);111this._editContext = EditContext.create(window);112this.setEditContextOnDomNode();113114this._screenReaderSupport = this._register(instantiationService.createInstance(ScreenReaderSupport, this.domNode, context, this._viewController));115116this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => {117this.logService.trace('NativeEditContext#copy');118119// !!!!!120// This is a workaround for what we think is an Electron bug where121// execCommand('copy') does not always work (it does not fire a clipboard event)122// !!!!!123// We signal that we have executed a copy command124CopyOptions.electronBugWorkaroundCopyEventHasFired = true;125126const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._context, this.logService, isFirefox);127this._onWillCopy.fire(copyEvent);128if (copyEvent.isHandled) {129return;130}131copyEvent.ensureClipboardGetsEditorData();132}));133this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => {134this.logService.trace('NativeEditContext#cut');135const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._context, this.logService, isFirefox);136this._onWillCut.fire(cutEvent);137if (cutEvent.isHandled) {138return;139}140// Pretend here we touched the text area, as the `cut` event will most likely141// result in a `selectionchange` event which we want to ignore142this._screenReaderSupport.onWillCut();143cutEvent.ensureClipboardGetsEditorData();144this.logService.trace('NativeEditContext#cut (before viewController.cut)');145this._viewController.cut();146}));147this._register(addDisposableListener(this.domNode.domNode, 'selectionchange', () => {148inputLatency.onSelectionChange();149}));150151this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e)));152this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e)));153this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e)));154this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e)));155this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => {156inputLatency.onBeforeInput();157if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') {158this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 });159}160}));161this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => {162this.logService.trace('NativeEditContext#paste');163const pasteEvent = createClipboardPasteEvent(e);164this._onWillPaste.fire(pasteEvent);165if (pasteEvent.isHandled) {166e.preventDefault();167return;168}169e.preventDefault();170if (!e.clipboardData) {171return;172}173this.logService.trace('NativeEditContext#paste with id : ', pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length);174if (!pasteEvent.text) {175return;176}177let pasteOnNewLine = false;178let multicursorText: string[] | null = null;179let mode: string | null = null;180if (pasteEvent.metadata) {181const options = this._context.configuration.options;182const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);183pasteOnNewLine = emptySelectionClipboard && !!pasteEvent.metadata.isFromEmptySelection;184multicursorText = typeof pasteEvent.metadata.multicursorText !== 'undefined' ? pasteEvent.metadata.multicursorText : null;185mode = pasteEvent.metadata.mode;186}187this.logService.trace('NativeEditContext#paste (before viewController.paste)');188this._viewController.paste(pasteEvent.text, pasteOnNewLine, multicursorText, mode);189}));190191// Edit context events192this._register(editContextAddDisposableListener(this._editContext, 'textformatupdate', (e) => this._handleTextFormatUpdate(e)));193this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e)));194let highSurrogateCharacter: string | undefined;195this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => {196inputLatency.onInput();197const text = e.text;198if (text.length === 1) {199const charCode = text.charCodeAt(0);200if (isHighSurrogate(charCode)) {201highSurrogateCharacter = text;202return;203}204if (isLowSurrogate(charCode) && highSurrogateCharacter) {205const textUpdateEvent: ITextUpdateEvent = {206text: highSurrogateCharacter + text,207selectionEnd: e.selectionEnd,208selectionStart: e.selectionStart,209updateRangeStart: e.updateRangeStart - 1,210updateRangeEnd: e.updateRangeEnd - 1211};212highSurrogateCharacter = undefined;213this._emitTypeEvent(this._viewController, textUpdateEvent);214return;215}216}217this._emitTypeEvent(this._viewController, e);218}));219this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => {220this._updateEditContext();221// Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state222// Updates the composition state of the cursor controller which determines behavior of typing with interceptors223this._viewController.compositionStart();224// Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers225this._context.viewModel.onCompositionStart();226}));227this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => {228this._updateEditContext();229// Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state230// Updates the composition state of the cursor controller which determines behavior of typing with interceptors231this._viewController.compositionEnd();232// Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers233this._context.viewModel.onCompositionEnd();234}));235let reenableTracking: boolean = false;236this._register(IME.onDidChange(() => {237if (IME.enabled && reenableTracking) {238this._focusTracker.resume();239this.domNode.focus();240reenableTracking = false;241}242if (!IME.enabled && this.isFocused()) {243this._focusTracker.pause();244this._imeTextArea.focus();245reenableTracking = true;246}247}));248this._register(NativeEditContextRegistry.register(ownerID, this));249}250251// --- Public methods ---252253public override dispose(): void {254// Force blue the dom node so can write in pane with no native edit context after disposal255this.domNode.domNode.editContext = undefined;256this.domNode.domNode.blur();257this.domNode.domNode.remove();258this._imeTextArea.domNode.remove();259super.dispose();260}261262public setAriaOptions(options: IEditorAriaOptions): void {263this._screenReaderSupport.setAriaOptions(options);264}265266/* Last rendered data needed for correct hit-testing and determining the mouse position.267* Without this, the selection will blink as incorrect mouse position is calculated */268public getLastRenderData(): Position | null {269return this._primarySelection.getPosition();270}271272public override onBeforeRender(viewportData: ViewportData): void {273// We need to read the position of the container dom node274// It is best to do this before we begin touching the DOM at all275// Because the sync layout will be fast if we do it here276this._parentBounds = this._parent.getBoundingClientRect();277}278279public override prepareRender(ctx: RenderingContext): void {280this._screenReaderSupport.prepareRender(ctx);281this._updateSelectionAndControlBoundsData(ctx);282}283284public render(ctx: RestrictedRenderingContext): void {285this._screenReaderSupport.render(ctx);286this._updateSelectionAndControlBounds();287}288289public override onCursorStateChanged(e: ViewCursorStateChangedEvent): boolean {290this._primarySelection = e.modelSelections[0] ?? new Selection(1, 1, 1, 1);291this._screenReaderSupport.onCursorStateChanged(e);292this._updateEditContext();293return true;294}295296public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean {297this._screenReaderSupport.onConfigurationChanged(e);298this._updateDomAttributes();299return true;300}301302public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {303// true for inline decorations that can end up relayouting text304return true;305}306307public override onFlushed(e: ViewFlushedEvent): boolean {308return true;309}310311public override onLinesChanged(e: ViewLinesChangedEvent): boolean {312this._updateEditContextOnLineChange(e.fromLineNumber, e.fromLineNumber + e.count - 1);313return true;314}315316public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {317this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);318return true;319}320321public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {322this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);323return true;324}325326private _updateEditContextOnLineChange(fromLineNumber: number, toLineNumber: number): void {327if (this._editContextPrimarySelection.endLineNumber < fromLineNumber || this._editContextPrimarySelection.startLineNumber > toLineNumber) {328return;329}330this._updateEditContext();331}332333public override onScrollChanged(e: ViewScrollChangedEvent): boolean {334this._scrollLeft = e.scrollLeft;335this._scrollTop = e.scrollTop;336return true;337}338339public override onZonesChanged(e: ViewZonesChangedEvent): boolean {340return true;341}342343public handleWillPaste(): void {344this.logService.trace('NativeEditContext#handleWillPaste');345this._prepareScreenReaderForPaste();346}347348private _prepareScreenReaderForPaste(): void {349this._screenReaderSupport.onWillPaste();350}351352public handleWillCopy(): void {353this.logService.trace('NativeEditContext#handleWillCopy');354this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement());355}356357public writeScreenReaderContent(): void {358this._screenReaderSupport.writeScreenReaderContent();359}360361public isFocused(): boolean {362return this._focusTracker.isFocused;363}364365public focus(): void {366this._focusTracker.focus();367368// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus369this.refreshFocusState();370}371372public refreshFocusState(): void {373this._focusTracker.refreshFocusState();374}375376// TODO: added as a workaround fix for https://github.com/microsoft/vscode/issues/229825377// When this issue will be fixed the following should be removed.378public setEditContextOnDomNode(): void {379const targetWindow = getWindow(this.domNode.domNode);380const targetWindowId = getWindowId(targetWindow);381if (this._targetWindowId !== targetWindowId) {382this.domNode.domNode.editContext = this._editContext;383this._targetWindowId = targetWindowId;384}385}386387// --- Private methods ---388389private _onKeyUp(e: KeyboardEvent) {390inputLatency.onKeyUp();391this._viewController.emitKeyUp(new StandardKeyboardEvent(e));392}393394private _onKeyDown(e: KeyboardEvent) {395inputLatency.onKeyDown();396const standardKeyboardEvent = new StandardKeyboardEvent(e);397// 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 further398if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) {399standardKeyboardEvent.stopPropagation();400}401this._viewController.emitKeyDown(standardKeyboardEvent);402}403404private _updateDomAttributes(): void {405const options = this._context.configuration.options;406this.domNode.domNode.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));407}408409private _updateEditContext(): void {410const editContextState = this._getNewEditContextState();411if (!editContextState) {412return;413}414this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text ?? ' ');415this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset);416this._editContextPrimarySelection = editContextState.editContextPrimarySelection;417this._previousEditContextSelection = new OffsetRange(editContextState.selectionStartOffset, editContextState.selectionEndOffset);418}419420private _emitTypeEvent(viewController: ViewController, e: ITextUpdateEvent): void {421if (!this._editContext) {422return;423}424const selectionEndOffset = this._previousEditContextSelection.endExclusive;425const selectionStartOffset = this._previousEditContextSelection.start;426this._previousEditContextSelection = new OffsetRange(e.selectionStart, e.selectionEnd);427428let replaceNextCharCnt = 0;429let replacePrevCharCnt = 0;430if (e.updateRangeEnd > selectionEndOffset) {431replaceNextCharCnt = e.updateRangeEnd - selectionEndOffset;432}433if (e.updateRangeStart < selectionStartOffset) {434replacePrevCharCnt = selectionStartOffset - e.updateRangeStart;435}436let text = '';437if (selectionStartOffset < e.updateRangeStart) {438text += this._editContext.text.substring(selectionStartOffset, e.updateRangeStart);439}440text += e.text;441if (selectionEndOffset > e.updateRangeEnd) {442text += this._editContext.text.substring(e.updateRangeEnd, selectionEndOffset);443}444let positionDelta = 0;445if (e.selectionStart === e.selectionEnd && selectionStartOffset === selectionEndOffset) {446positionDelta = e.selectionStart - (e.updateRangeStart + e.text.length);447}448const typeInput: ITypeData = {449text,450replacePrevCharCnt,451replaceNextCharCnt,452positionDelta453};454this._onType(viewController, typeInput);455}456457private _onType(viewController: ViewController, typeInput: ITypeData): void {458if (typeInput.replacePrevCharCnt || typeInput.replaceNextCharCnt || typeInput.positionDelta) {459viewController.compositionType(typeInput.text, typeInput.replacePrevCharCnt, typeInput.replaceNextCharCnt, typeInput.positionDelta);460} else {461viewController.type(typeInput.text);462}463}464465private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; editContextPrimarySelection: Selection } | undefined {466const editContextPrimarySelection = this._primarySelection;467const model = this._context.viewModel.model;468if (!model.isValidRange(editContextPrimarySelection)) {469return;470}471const primarySelectionStartLine = editContextPrimarySelection.startLineNumber;472const primarySelectionEndLine = editContextPrimarySelection.endLineNumber;473const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine);474const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber);475const text = model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined);476const selectionStartOffset = editContextPrimarySelection.startColumn - 1;477const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber;478return {479text,480selectionStartOffset,481selectionEndOffset,482editContextPrimarySelection483};484}485486private _editContextStartPosition(): Position {487return new Position(this._editContextPrimarySelection.startLineNumber, 1);488}489490private _handleTextFormatUpdate(e: TextFormatUpdateEvent): void {491if (!this._editContext) {492return;493}494const formats = e.getTextFormats();495const editContextStartPosition = this._editContextStartPosition();496const decorations: IModelDeltaDecoration[] = [];497formats.forEach(f => {498const textModel = this._context.viewModel.model;499const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition);500const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart);501const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd);502const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration);503const thickness = f.underlineThickness.toLowerCase();504let decorationClassName: string = CompositionClassName.NONE;505switch (thickness) {506case 'thin':507decorationClassName = CompositionClassName.SECONDARY;508break;509case 'thick':510decorationClassName = CompositionClassName.PRIMARY;511break;512}513decorations.push({514range: decorationRange,515options: {516description: 'textFormatDecoration',517inlineClassName: decorationClassName,518}519});520});521this._decorations = this._context.viewModel.model.deltaDecorations(this._decorations, decorations);522}523524private _linesVisibleRanges: HorizontalPosition | null = null;525private _updateSelectionAndControlBoundsData(ctx: RenderingContext): void {526const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);527if (this._primarySelection.isEmpty()) {528const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition());529this._linesVisibleRanges = linesVisibleRanges;530} else {531this._linesVisibleRanges = null;532}533}534535private _updateSelectionAndControlBounds() {536const options = this._context.configuration.options;537const contentLeft = options.get(EditorOption.layoutInfo).contentLeft;538539const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);540const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber);541const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber);542543// !!! Make sure this doesn't force an extra layout544// !!! by using the cached parent bounds read in onBeforeRender545const parentBounds = this._parentBounds!;546const top = parentBounds.top + verticalOffsetStart - this._scrollTop;547const height = verticalOffsetEnd - verticalOffsetStart;548let left = parentBounds.left + contentLeft - this._scrollLeft;549let width: number;550551if (this._primarySelection.isEmpty()) {552if (this._linesVisibleRanges) {553left += this._linesVisibleRanges.left;554}555width = 0;556} else {557width = parentBounds.width - contentLeft;558}559560const selectionBounds = new DOMRect(left, top, width, height);561this._editContext.updateSelectionBounds(selectionBounds);562this._editContext.updateControlBounds(selectionBounds);563}564565private _updateCharacterBounds(e: CharacterBoundsUpdateEvent): void {566const options = this._context.configuration.options;567const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;568const contentLeft = options.get(EditorOption.layoutInfo).contentLeft;569const parentBounds = this._parentBounds!;570571const characterBounds: DOMRect[] = [];572const offsetTransformer = new PositionOffsetTransformer(this._editContext.text);573for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) {574const editContextStartPosition = offsetTransformer.getPosition(offset);575const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1;576const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column);577const characterEndPosition = characterStartPosition.delta(0, 1);578const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition);579const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange);580const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? [];581const lineNumber = characterViewRange.startLineNumber;582const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);583const top = parentBounds.top + characterVerticalOffset - this._scrollTop;584585let left = 0;586let width = typicalHalfWidthCharacterWidth;587if (characterLinesVisibleRanges.length > 0) {588for (const visibleRange of characterLinesVisibleRanges[0].ranges) {589left = visibleRange.left;590width = visibleRange.width;591break;592}593}594const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);595characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight));596}597this._editContext.updateCharacterBounds(e.rangeStart, characterBounds);598}599}600601602