Path: blob/main/src/vs/editor/browser/controller/mouseTarget.ts
5221 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 { IPointerHandlerHelper } from './mouseHandler.js';6import { IMouseTargetContentEmptyData, IMouseTargetMarginData, IMouseTarget, IMouseTargetContentEmpty, IMouseTargetContentText, IMouseTargetContentWidget, IMouseTargetMargin, IMouseTargetOutsideEditor, IMouseTargetOverlayWidget, IMouseTargetScrollbar, IMouseTargetTextarea, IMouseTargetUnknown, IMouseTargetViewZone, IMouseTargetContentTextData, IMouseTargetViewZoneData, MouseTargetType } from '../editorBrowser.js';7import { ClientCoordinates, EditorMouseEvent, EditorPagePosition, PageCoordinates, CoordinatesRelativeToEditor } from '../editorDom.js';8import { PartFingerprint, PartFingerprints } from '../view/viewPart.js';9import { ViewLine } from '../viewParts/viewLines/viewLine.js';10import { IViewCursorRenderData } from '../viewParts/viewCursors/viewCursor.js';11import { EditorLayoutInfo, EditorOption } from '../../common/config/editorOptions.js';12import { Position } from '../../common/core/position.js';13import { Range as EditorRange } from '../../common/core/range.js';14import { HorizontalPosition } from '../view/renderingContext.js';15import { ViewContext } from '../../common/viewModel/viewContext.js';16import { IViewModel } from '../../common/viewModel.js';17import { CursorColumns } from '../../common/core/cursorColumns.js';18import * as dom from '../../../base/browser/dom.js';19import { AtomicTabMoveOperations, Direction } from '../../common/cursor/cursorAtomicMoveOperations.js';20import { PositionAffinity, TextDirection } from '../../common/model.js';21import { InjectedText } from '../../common/modelLineProjectionData.js';22import { Mutable } from '../../../base/common/types.js';23import { Lazy } from '../../../base/common/lazy.js';24import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js';2526const enum HitTestResultType {27Unknown,28Content,29}3031class UnknownHitTestResult {32readonly type = HitTestResultType.Unknown;33constructor(34readonly hitTarget: HTMLElement | null = null35) { }36}3738class ContentHitTestResult {39readonly type = HitTestResultType.Content;4041get hitTarget(): HTMLElement { return this.spanNode; }4243constructor(44readonly position: Position,45readonly spanNode: HTMLElement,46readonly injectedText: InjectedText | null,47) { }48}4950type HitTestResult = UnknownHitTestResult | ContentHitTestResult;5152namespace HitTestResult {53export function createFromDOMInfo(ctx: HitTestContext, spanNode: HTMLElement, offset: number): HitTestResult {54const position = ctx.getPositionFromDOMInfo(spanNode, offset);55if (position) {56return new ContentHitTestResult(position, spanNode, null);57}58return new UnknownHitTestResult(spanNode);59}60}6162export class PointerHandlerLastRenderData {63constructor(64public readonly lastViewCursorsRenderData: IViewCursorRenderData[],65public readonly lastTextareaPosition: Position | null66) { }67}6869export class MouseTarget {7071private static _deduceRage(position: Position): EditorRange;72private static _deduceRage(position: Position, range: EditorRange | null): EditorRange;73private static _deduceRage(position: Position | null): EditorRange | null;74private static _deduceRage(position: Position | null, range: EditorRange | null = null): EditorRange | null {75if (!range && position) {76return new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);77}78return range ?? null;79}80public static createUnknown(element: HTMLElement | null, mouseColumn: number, position: Position | null): IMouseTargetUnknown {81return { type: MouseTargetType.UNKNOWN, element, mouseColumn, position, range: this._deduceRage(position) };82}83public static createTextarea(element: HTMLElement | null, mouseColumn: number): IMouseTargetTextarea {84return { type: MouseTargetType.TEXTAREA, element, mouseColumn, position: null, range: null };85}86public static createMargin(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, element: HTMLElement | null, mouseColumn: number, position: Position, range: EditorRange, detail: IMouseTargetMarginData): IMouseTargetMargin {87return { type, element, mouseColumn, position, range, detail };88}89public static createViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, element: HTMLElement | null, mouseColumn: number, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone {90return { type, element, mouseColumn, position, range: this._deduceRage(position), detail };91}92public static createContentText(element: HTMLElement | null, mouseColumn: number, position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {93return { type: MouseTargetType.CONTENT_TEXT, element, mouseColumn, position, range: this._deduceRage(position, range), detail };94}95public static createContentEmpty(element: HTMLElement | null, mouseColumn: number, position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {96return { type: MouseTargetType.CONTENT_EMPTY, element, mouseColumn, position, range: this._deduceRage(position), detail };97}98public static createContentWidget(element: HTMLElement | null, mouseColumn: number, detail: string): IMouseTargetContentWidget {99return { type: MouseTargetType.CONTENT_WIDGET, element, mouseColumn, position: null, range: null, detail };100}101public static createScrollbar(element: HTMLElement | null, mouseColumn: number, position: Position): IMouseTargetScrollbar {102return { type: MouseTargetType.SCROLLBAR, element, mouseColumn, position, range: this._deduceRage(position) };103}104public static createOverlayWidget(element: HTMLElement | null, mouseColumn: number, detail: string): IMouseTargetOverlayWidget {105return { type: MouseTargetType.OVERLAY_WIDGET, element, mouseColumn, position: null, range: null, detail };106}107public static createOutsideEditor(mouseColumn: number, position: Position, outsidePosition: 'above' | 'below' | 'left' | 'right', outsideDistance: number): IMouseTargetOutsideEditor {108return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance };109}110111private static _typeToString(type: MouseTargetType): string {112if (type === MouseTargetType.TEXTAREA) {113return 'TEXTAREA';114}115if (type === MouseTargetType.GUTTER_GLYPH_MARGIN) {116return 'GUTTER_GLYPH_MARGIN';117}118if (type === MouseTargetType.GUTTER_LINE_NUMBERS) {119return 'GUTTER_LINE_NUMBERS';120}121if (type === MouseTargetType.GUTTER_LINE_DECORATIONS) {122return 'GUTTER_LINE_DECORATIONS';123}124if (type === MouseTargetType.GUTTER_VIEW_ZONE) {125return 'GUTTER_VIEW_ZONE';126}127if (type === MouseTargetType.CONTENT_TEXT) {128return 'CONTENT_TEXT';129}130if (type === MouseTargetType.CONTENT_EMPTY) {131return 'CONTENT_EMPTY';132}133if (type === MouseTargetType.CONTENT_VIEW_ZONE) {134return 'CONTENT_VIEW_ZONE';135}136if (type === MouseTargetType.CONTENT_WIDGET) {137return 'CONTENT_WIDGET';138}139if (type === MouseTargetType.OVERVIEW_RULER) {140return 'OVERVIEW_RULER';141}142if (type === MouseTargetType.SCROLLBAR) {143return 'SCROLLBAR';144}145if (type === MouseTargetType.OVERLAY_WIDGET) {146return 'OVERLAY_WIDGET';147}148return 'UNKNOWN';149}150151public static toString(target: IMouseTarget): string {152return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify((target as unknown as Record<string, unknown>).detail);153}154}155156class ElementPath {157158public static isTextArea(path: Uint8Array): boolean {159return (160path.length === 2161&& path[0] === PartFingerprint.OverflowGuard162&& path[1] === PartFingerprint.TextArea163);164}165166public static isChildOfViewLines(path: Uint8Array): boolean {167return (168path.length >= 4169&& path[0] === PartFingerprint.OverflowGuard170&& path[3] === PartFingerprint.ViewLines171);172}173174public static isStrictChildOfViewLines(path: Uint8Array): boolean {175return (176path.length > 4177&& path[0] === PartFingerprint.OverflowGuard178&& path[3] === PartFingerprint.ViewLines179);180}181182public static isChildOfScrollableElement(path: Uint8Array): boolean {183return (184path.length >= 2185&& path[0] === PartFingerprint.OverflowGuard186&& path[1] === PartFingerprint.ScrollableElement187);188}189190public static isChildOfMinimap(path: Uint8Array): boolean {191return (192path.length >= 2193&& path[0] === PartFingerprint.OverflowGuard194&& path[1] === PartFingerprint.Minimap195);196}197198public static isChildOfContentWidgets(path: Uint8Array): boolean {199return (200path.length >= 4201&& path[0] === PartFingerprint.OverflowGuard202&& path[3] === PartFingerprint.ContentWidgets203);204}205206public static isChildOfOverflowGuard(path: Uint8Array): boolean {207return (208path.length >= 1209&& path[0] === PartFingerprint.OverflowGuard210);211}212213public static isChildOfOverflowingContentWidgets(path: Uint8Array): boolean {214return (215path.length >= 1216&& path[0] === PartFingerprint.OverflowingContentWidgets217);218}219220public static isChildOfOverlayWidgets(path: Uint8Array): boolean {221return (222path.length >= 2223&& path[0] === PartFingerprint.OverflowGuard224&& path[1] === PartFingerprint.OverlayWidgets225);226}227228public static isChildOfOverflowingOverlayWidgets(path: Uint8Array): boolean {229return (230path.length >= 1231&& path[0] === PartFingerprint.OverflowingOverlayWidgets232);233}234}235236export class HitTestContext {237238public readonly viewModel: IViewModel;239public readonly layoutInfo: EditorLayoutInfo;240public readonly viewDomNode: HTMLElement;241public readonly viewLinesGpu: ViewLinesGpu | undefined;242public readonly lineHeight: number;243public readonly stickyTabStops: boolean;244public readonly typicalHalfwidthCharacterWidth: number;245public readonly lastRenderData: PointerHandlerLastRenderData;246247private readonly _context: ViewContext;248private readonly _viewHelper: IPointerHandlerHelper;249250constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastRenderData: PointerHandlerLastRenderData) {251this.viewModel = context.viewModel;252const options = context.configuration.options;253this.layoutInfo = options.get(EditorOption.layoutInfo);254this.viewDomNode = viewHelper.viewDomNode;255this.viewLinesGpu = viewHelper.viewLinesGpu;256this.lineHeight = options.get(EditorOption.lineHeight);257this.stickyTabStops = options.get(EditorOption.stickyTabStops);258this.typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;259this.lastRenderData = lastRenderData;260this._context = context;261this._viewHelper = viewHelper;262}263264public getZoneAtCoord(mouseVerticalOffset: number): IMouseTargetViewZoneData | null {265return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset);266}267268public static getZoneAtCoord(context: ViewContext, mouseVerticalOffset: number): IMouseTargetViewZoneData | null {269// The target is either a view zone or the empty space after the last view-line270const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);271272if (viewZoneWhitespace) {273const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2;274const lineCount = context.viewModel.getLineCount();275let positionBefore: Position | null = null;276let position: Position | null;277let positionAfter: Position | null = null;278279if (viewZoneWhitespace.afterLineNumber !== lineCount) {280// There are more lines after this view zone281positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);282}283if (viewZoneWhitespace.afterLineNumber > 0) {284// There are more lines above this view zone285positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.viewModel.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));286}287288if (positionAfter === null) {289position = positionBefore;290} else if (positionBefore === null) {291position = positionAfter;292} else if (mouseVerticalOffset < viewZoneMiddle) {293position = positionBefore;294} else {295position = positionAfter;296}297298return {299viewZoneId: viewZoneWhitespace.id,300afterLineNumber: viewZoneWhitespace.afterLineNumber,301positionBefore: positionBefore,302positionAfter: positionAfter,303position: position!304};305}306return null;307}308309public getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean } {310if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {311// Below the last line312const lineNumber = this._context.viewModel.getLineCount();313const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);314return {315range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),316isAfterLines: true317};318}319320const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);321const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);322return {323range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),324isAfterLines: false325};326}327328public getLineNumberAtVerticalOffset(mouseVerticalOffset: number): number {329return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);330}331332public isAfterLines(mouseVerticalOffset: number): boolean {333return this._context.viewLayout.isAfterLines(mouseVerticalOffset);334}335336public isInTopPadding(mouseVerticalOffset: number): boolean {337return this._context.viewLayout.isInTopPadding(mouseVerticalOffset);338}339340public isInBottomPadding(mouseVerticalOffset: number): boolean {341return this._context.viewLayout.isInBottomPadding(mouseVerticalOffset);342}343344public getVerticalOffsetForLineNumber(lineNumber: number): number {345return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);346}347348public findAttribute(element: Element, attr: string): string | null {349return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);350}351352private static _findAttribute(element: Element, attr: string, stopAt: Element): string | null {353while (element && element !== element.ownerDocument.body) {354if (element.hasAttribute && element.hasAttribute(attr)) {355return element.getAttribute(attr);356}357if (element === stopAt) {358return null;359}360element = <Element>element.parentNode;361}362return null;363}364365public getLineWidth(lineNumber: number): number {366return this._viewHelper.getLineWidth(lineNumber);367}368369public isRtl(lineNumber: number): boolean {370return this.viewModel.getTextDirection(lineNumber) === TextDirection.RTL;371372}373374public visibleRangeForPosition(lineNumber: number, column: number): HorizontalPosition | null {375return this._viewHelper.visibleRangeForPosition(lineNumber, column);376}377378public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position | null {379return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);380}381382public getCurrentScrollTop(): number {383return this._context.viewLayout.getCurrentScrollTop();384}385386public getCurrentScrollLeft(): number {387return this._context.viewLayout.getCurrentScrollLeft();388}389}390391abstract class BareHitTestRequest {392393public readonly editorPos: EditorPagePosition;394public readonly pos: PageCoordinates;395public readonly relativePos: CoordinatesRelativeToEditor;396public readonly mouseVerticalOffset: number;397public readonly isInMarginArea: boolean;398public readonly isInContentArea: boolean;399public readonly mouseContentHorizontalOffset: number;400401protected readonly mouseColumn: number;402403constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor) {404this.editorPos = editorPos;405this.pos = pos;406this.relativePos = relativePos;407408this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y);409this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft;410this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft);411this.isInContentArea = !this.isInMarginArea;412this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));413}414}415416class HitTestRequest extends BareHitTestRequest {417private readonly _ctx: HitTestContext;418private readonly _eventTarget: HTMLElement | null;419public readonly hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this));420private _useHitTestTarget: boolean;421private _targetPathCacheElement: HTMLElement | null = null;422private _targetPathCacheValue: Uint8Array = new Uint8Array(0);423424public get target(): HTMLElement | null {425if (this._useHitTestTarget) {426return this.hitTestResult.value.hitTarget;427}428return this._eventTarget;429}430431public get targetPath(): Uint8Array {432if (this._targetPathCacheElement !== this.target) {433this._targetPathCacheElement = this.target;434this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode);435}436return this._targetPathCacheValue;437}438439constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, eventTarget: HTMLElement | null) {440super(ctx, editorPos, pos, relativePos);441this._ctx = ctx;442this._eventTarget = eventTarget;443444// If no event target is passed in, we will use the hit test target445const hasEventTarget = Boolean(this._eventTarget);446this._useHitTestTarget = !hasEventTarget;447}448449public override toString(): string {450return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? this.target.outerHTML : null}`;451}452453public get wouldBenefitFromHitTestTargetSwitch(): boolean {454return (455!this._useHitTestTarget456&& this.hitTestResult.value.hitTarget !== null457&& this.target !== this.hitTestResult.value.hitTarget458);459}460461public switchToHitTestTarget(): void {462this._useHitTestTarget = true;463}464465private _getMouseColumn(position: Position | null = null): number {466if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {467// Most likely, the line contains foreign decorations...468return CursorColumns.visibleColumnFromColumn(this._ctx.viewModel.getLineContent(position.lineNumber), position.column, this._ctx.viewModel.model.getOptions().tabSize) + 1;469}470return this.mouseColumn;471}472473public fulfillUnknown(position: Position | null = null): IMouseTargetUnknown {474return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position);475}476public fulfillTextarea(): IMouseTargetTextarea {477return MouseTarget.createTextarea(this.target, this._getMouseColumn());478}479public fulfillMargin(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMouseTargetMarginData): IMouseTargetMargin {480return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail);481}482public fulfillViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone {483// Always return the usual mouse column for a view zone.484return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(), position, detail);485}486public fulfillContentText(position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {487return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);488}489public fulfillContentEmpty(position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {490return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);491}492public fulfillContentWidget(detail: string): IMouseTargetContentWidget {493return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);494}495public fulfillScrollbar(position: Position): IMouseTargetScrollbar {496return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);497}498public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget {499return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);500}501}502503interface ResolvedHitTestRequest extends HitTestRequest {504readonly target: HTMLElement;505}506507const EMPTY_CONTENT_AFTER_LINES: IMouseTargetContentEmptyData = { isAfterLines: true };508509function createEmptyContentDataInLines(horizontalDistanceToText: number): IMouseTargetContentEmptyData {510return {511isAfterLines: false,512horizontalDistanceToText: horizontalDistanceToText513};514}515516export class MouseTargetFactory {517518private readonly _context: ViewContext;519private readonly _viewHelper: IPointerHandlerHelper;520521constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) {522this._context = context;523this._viewHelper = viewHelper;524}525526public mouseTargetIsWidget(e: EditorMouseEvent): boolean {527const t = <Element>e.target;528const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);529530// Is it a content widget?531if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {532return true;533}534535// Is it an overlay widget?536if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {537return true;538}539540return false;541}542543public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null): IMouseTarget {544const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);545const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);546try {547const r = MouseTargetFactory._createMouseTarget(ctx, request);548549if (r.type === MouseTargetType.CONTENT_TEXT) {550// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.551if (ctx.stickyTabStops && r.position !== null) {552const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);553const range = EditorRange.fromPositions(position, position).plusRange(r.range);554return request.fulfillContentText(position, range, r.detail);555}556}557558// console.log(MouseTarget.toString(r));559return r;560} catch (err) {561// console.log(err);562return request.fulfillUnknown();563}564}565566private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget {567568// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);569570if (request.target === null) {571// No target572return request.fulfillUnknown();573}574575// we know for a fact that request.target is not null576const resolvedRequest = <ResolvedHitTestRequest>request;577578let result: IMouseTarget | null = null;579580if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {581// We only render dom nodes inside the overflow guard or in the overflowing content widgets582result = result || request.fulfillUnknown();583}584585result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);586result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);587result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);588result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);589result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);590result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);591result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);592result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);593result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);594result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);595596return (result || request.fulfillUnknown());597}598599private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {600// Is it a content widget?601if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {602const widgetId = ctx.findAttribute(request.target, 'widgetId');603if (widgetId) {604return request.fulfillContentWidget(widgetId);605} else {606return request.fulfillUnknown();607}608}609return null;610}611612private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {613// Is it an overlay widget?614if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {615const widgetId = ctx.findAttribute(request.target, 'widgetId');616if (widgetId) {617return request.fulfillOverlayWidget(widgetId);618} else {619return request.fulfillUnknown();620}621}622return null;623}624625private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {626627if (request.target) {628// Check if we've hit a painted cursor629const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;630631for (const d of lastViewCursorsRenderData) {632633if (request.target === d.domNode) {634return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });635}636}637}638639if (request.isInContentArea) {640// Edge has a bug when hit-testing the exact position of a cursor,641// instead of returning the correct dom node, it returns the642// first or last rendered view line dom node, therefore help it out643// and first check if we are on top of a cursor644645const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;646const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;647const mouseVerticalOffset = request.mouseVerticalOffset;648649for (const d of lastViewCursorsRenderData) {650651if (mouseContentHorizontalOffset < d.contentLeft) {652// mouse position is to the left of the cursor653continue;654}655if (mouseContentHorizontalOffset > d.contentLeft + d.width) {656// mouse position is to the right of the cursor657continue;658}659660const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);661662if (663cursorVerticalOffset <= mouseVerticalOffset664&& mouseVerticalOffset <= cursorVerticalOffset + d.height665) {666return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });667}668}669}670671return null;672}673674private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {675const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);676if (viewZoneData) {677const mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE);678return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);679}680681return null;682}683684private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {685// Is it the textarea?686if (ElementPath.isTextArea(request.targetPath)) {687if (ctx.lastRenderData.lastTextareaPosition) {688return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });689}690return request.fulfillTextarea();691}692return null;693}694695private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {696if (request.isInMarginArea) {697const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);698const pos = res.range.getStartPosition();699let offset = Math.abs(request.relativePos.x);700const detail: Mutable<IMouseTargetMarginData> = {701isAfterLines: res.isAfterLines,702glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,703glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,704lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,705offsetX: offset706};707708offset -= ctx.layoutInfo.glyphMarginLeft;709710if (offset <= ctx.layoutInfo.glyphMarginWidth) {711// On the glyph margin712const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());713const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);714detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];715return request.fulfillMargin(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail);716}717offset -= ctx.layoutInfo.glyphMarginWidth;718719if (offset <= ctx.layoutInfo.lineNumbersWidth) {720// On the line numbers721return request.fulfillMargin(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail);722}723offset -= ctx.layoutInfo.lineNumbersWidth;724725// On the line decorations726return request.fulfillMargin(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail);727}728return null;729}730731private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {732if (!ElementPath.isChildOfViewLines(request.targetPath)) {733return null;734}735736if (ctx.isInTopPadding(request.mouseVerticalOffset)) {737return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);738}739740// Check if it is below any lines and any view zones741if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {742// This most likely indicates it happened after the last view-line743const lineCount = ctx.viewModel.getLineCount();744const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);745return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);746}747748// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)749// See https://github.com/microsoft/vscode/issues/46942750if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {751const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);752const lineLength = ctx.viewModel.getLineLength(lineNumber);753const lineWidth = ctx.getLineWidth(lineNumber);754if (lineLength === 0) {755const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);756return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);757}758759const isRtl = ctx.isRtl(lineNumber);760if (isRtl) {761if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {762const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);763const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));764return request.fulfillContentEmpty(pos, detail);765}766} else if (request.mouseContentHorizontalOffset >= lineWidth) {767const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);768const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));769return request.fulfillContentEmpty(pos, detail);770}771} else {772if (ctx.viewLinesGpu) {773const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);774if (ctx.viewModel.getLineLength(lineNumber) === 0) {775const lineWidth = ctx.getLineWidth(lineNumber);776const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);777return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);778}779780const lineWidth = ctx.getLineWidth(lineNumber);781const isRtl = ctx.isRtl(lineNumber);782if (isRtl) {783if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {784const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);785const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));786return request.fulfillContentEmpty(pos, detail);787}788} else if (request.mouseContentHorizontalOffset >= lineWidth) {789const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);790const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));791return request.fulfillContentEmpty(pos, detail);792}793794const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);795if (position) {796const detail: IMouseTargetContentTextData = {797injectedText: null,798mightBeForeignElement: false799};800return request.fulfillContentText(position, EditorRange.fromPositions(position, position), detail);801}802}803}804805// Do the hit test (if not already done)806const hitTestResult = request.hitTestResult.value;807808if (hitTestResult.type === HitTestResultType.Content) {809return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);810}811812// We didn't hit content...813if (request.wouldBenefitFromHitTestTargetSwitch) {814// We actually hit something different... Give it one last change by trying again with this new target815request.switchToHitTestTarget();816return this._createMouseTarget(ctx, request);817}818819// We have tried everything...820return request.fulfillUnknown();821}822823private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {824if (ElementPath.isChildOfMinimap(request.targetPath)) {825const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);826const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);827return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));828}829return null;830}831832private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {833if (ElementPath.isChildOfScrollableElement(request.targetPath)) {834if (request.target && request.target.nodeType === 1) {835const className = request.target.className;836if (className && /\b(slider|scrollbar)\b/.test(className)) {837const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);838const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);839return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));840}841}842}843return null;844}845846private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {847// Is it the overview ruler?848// Is it a child of the scrollable element?849if (ElementPath.isChildOfScrollableElement(request.targetPath)) {850const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);851const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);852return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));853}854855return null;856}857858public getMouseColumn(relativePos: CoordinatesRelativeToEditor): number {859const options = this._context.configuration.options;860const layoutInfo = options.get(EditorOption.layoutInfo);861const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;862return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth);863}864865public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number {866if (mouseContentHorizontalOffset < 0) {867return 1;868}869const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);870return (chars + 1);871}872873private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): IMouseTarget {874const lineNumber = pos.lineNumber;875const column = pos.column;876877const lineWidth = ctx.getLineWidth(lineNumber);878879if (request.mouseContentHorizontalOffset > lineWidth) {880const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);881return request.fulfillContentEmpty(pos, detail);882}883884const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);885886if (!visibleRange) {887return request.fulfillUnknown(pos);888}889890const columnHorizontalOffset = visibleRange.left;891892if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {893return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });894}895896// Let's define a, b, c and check if the offset is in between them...897interface OffsetColumn { offset: number; column: number }898899const points: OffsetColumn[] = [];900points.push({ offset: visibleRange.left, column: column });901if (column > 1) {902const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);903if (visibleRange) {904points.push({ offset: visibleRange.left, column: column - 1 });905}906}907const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);908if (column < lineMaxColumn) {909const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);910if (visibleRange) {911points.push({ offset: visibleRange.left, column: column + 1 });912}913}914915points.sort((a, b) => a.offset - b.offset);916917const mouseCoordinates = request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode));918const spanNodeClientRect = spanNode.getBoundingClientRect();919const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);920921let rng: EditorRange | null = null;922923for (let i = 1; i < points.length; i++) {924const prev = points[i - 1];925const curr = points[i];926if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {927rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);928929// See https://github.com/microsoft/vscode/issues/152819930// Due to the use of zwj, the browser's hit test result is skewed towards the left931// Here we try to correct that if the mouse horizontal offset is closer to the right than the left932933const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);934const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);935936pos = (937prevDelta < nextDelta938? new Position(lineNumber, prev.column)939: new Position(lineNumber, curr.column)940);941942break;943}944}945946return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });947}948949/**950* Most probably WebKit browsers and Edge951*/952private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {953954// In Chrome, especially on Linux it is possible to click between lines,955// so try to adjust the `hity` below so that it lands in the center of a line956const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);957const lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);958const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;959960const isBelowLastLine = (961lineNumber === ctx.viewModel.getLineCount()962&& request.mouseVerticalOffset > lineEndVerticalOffset963);964965if (!isBelowLastLine) {966const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);967let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);968969if (adjustedPageY <= request.editorPos.y) {970adjustedPageY = request.editorPos.y + 1;971}972if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {973adjustedPageY = request.editorPos.y + request.editorPos.height - 1;974}975976const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);977978const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));979if (r.type === HitTestResultType.Content) {980return r;981}982}983984// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)985return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));986}987988private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {989const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);990let range: Range;991if (shadowRoot) {992// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any993if (typeof (<any>shadowRoot).caretRangeFromPoint === 'undefined') {994range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);995} else {996// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any997range = (<any>shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY);998}999} else {1000// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any1001range = (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY);1002}10031004if (!range || !range.startContainer) {1005return new UnknownHitTestResult();1006}10071008// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span1009const startContainer = range.startContainer;10101011if (startContainer.nodeType === startContainer.TEXT_NODE) {1012// startContainer is expected to be the token text1013const parent1 = startContainer.parentNode; // expected to be the token span1014const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span1015const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div1016const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;10171018if (parent3ClassName === ViewLine.CLASS_NAME) {1019return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>parent1, range.startOffset);1020} else {1021return new UnknownHitTestResult(<HTMLElement>startContainer.parentNode);1022}1023} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {1024// startContainer is expected to be the token span1025const parent1 = startContainer.parentNode; // expected to be the view line container span1026const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div1027const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;10281029if (parent2ClassName === ViewLine.CLASS_NAME) {1030return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>startContainer, (<HTMLElement>startContainer).textContent.length);1031} else {1032return new UnknownHitTestResult(<HTMLElement>startContainer);1033}1034}10351036return new UnknownHitTestResult();1037}10381039/**1040* Most probably Gecko1041*/1042private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {1043// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any1044const hitResult: { offsetNode: Node; offset: number } = (<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY);10451046if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {1047// offsetNode is expected to be the token text1048const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span1049const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span1050const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div1051const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;10521053if (parent3ClassName === ViewLine.CLASS_NAME) {1054return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode.parentNode, hitResult.offset);1055} else {1056return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode.parentNode);1057}1058}10591060// For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration1061// Some other times, it returns the `<span>` with the inline decoration1062if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {1063const parent1 = hitResult.offsetNode.parentNode;1064const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (<HTMLElement>parent1).className : null;1065const parent2 = parent1 ? parent1.parentNode : null;1066const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;10671068if (parent1ClassName === ViewLine.CLASS_NAME) {1069// it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration1070const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];1071if (tokenSpan) {1072return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>tokenSpan, 0);1073}1074} else if (parent2ClassName === ViewLine.CLASS_NAME) {1075// it returned the `<span>` with the inline decoration1076return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode, 0);1077}1078}10791080return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode);1081}10821083private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position {1084const lineContent = viewModel.getLineContent(position.lineNumber);1085const { tabSize } = viewModel.model.getOptions();1086const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest);1087if (newPosition !== -1) {1088return new Position(position.lineNumber, newPosition + 1);1089}1090return position;1091}10921093public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {10941095let result: HitTestResult = new UnknownHitTestResult();1096// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any1097if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {1098result = this._doHitTestWithCaretRangeFromPoint(ctx, request);1099// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any1100} else if ((<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint) {1101result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));1102}1103if (result.type === HitTestResultType.Content) {1104const injectedText = ctx.viewModel.getInjectedTextAt(result.position);11051106const normalizedPosition = ctx.viewModel.normalizePosition(result.position, PositionAffinity.None);1107if (injectedText || !normalizedPosition.equals(result.position)) {1108result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);1109}1110}1111return result;1112}1113}11141115function shadowCaretRangeFromPoint(shadowRoot: ShadowRoot, x: number, y: number): Range {1116const range = document.createRange();11171118// Get the element under the point1119// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any1120let el: HTMLElement | null = (<any>shadowRoot).elementFromPoint(x, y);1121// When el is not null, it may be div.monaco-mouse-cursor-text Element, which has not childNodes, we don't need to handle it.1122if (el?.hasChildNodes()) {1123// Get the last child of the element until its firstChild is a text node1124// This assumes that the pointer is on the right of the line, out of the tokens1125// and that we want to get the offset of the last token of the line1126while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {1127el = <HTMLElement>el.lastChild;1128}11291130// Grab its rect1131const rect = el.getBoundingClientRect();11321133// And its font (the computed shorthand font property might be empty, see #3217)1134const elWindow = dom.getWindow(el);1135const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');1136const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');1137const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');1138const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');1139const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');1140const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');1141const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;11421143// And also its txt content1144const text = el.innerText;11451146// Position the pixel cursor at the left of the element1147let pixelCursor = rect.left;1148let offset = 0;1149let step: number;11501151// If the point is on the right of the box put the cursor after the last character1152if (x > rect.left + rect.width) {1153offset = text.length;1154} else {1155const charWidthReader = CharWidthReader.getInstance();1156// Goes through all the characters of the innerText, and checks if the x of the point1157// belongs to the character.1158for (let i = 0; i < text.length + 1; i++) {1159// The step is half the width of the character1160step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;1161// Move to the center of the character1162pixelCursor += step;1163// If the x of the point is smaller that the position of the cursor, the point is over that character1164if (x < pixelCursor) {1165offset = i;1166break;1167}1168// Move between the current character and the next1169pixelCursor += step;1170}1171}11721173// Creates a range with the text node of the element and set the offset found1174range.setStart(el.firstChild!, offset);1175range.setEnd(el.firstChild!, offset);1176}11771178return range;1179}11801181class CharWidthReader {1182private static _INSTANCE: CharWidthReader | null = null;11831184public static getInstance(): CharWidthReader {1185if (!CharWidthReader._INSTANCE) {1186CharWidthReader._INSTANCE = new CharWidthReader();1187}1188return CharWidthReader._INSTANCE;1189}11901191private readonly _cache: { [cacheKey: string]: number };1192private readonly _canvas: HTMLCanvasElement;11931194private constructor() {1195this._cache = {};1196this._canvas = document.createElement('canvas');1197}11981199public getCharWidth(char: string, font: string): number {1200const cacheKey = char + font;1201if (this._cache[cacheKey]) {1202return this._cache[cacheKey];1203}12041205const context = this._canvas.getContext('2d')!;1206context.font = font;1207const metrics = context.measureText(char);1208const width = metrics.width;1209this._cache[cacheKey] = width;1210return width;1211}1212}121312141215