Path: blob/main/src/vs/editor/browser/controller/mouseTarget.ts
3294 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((<any>target).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 ? (<HTMLElement>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 {483return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(position), position, detail);484}485public fulfillContentText(position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {486return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);487}488public fulfillContentEmpty(position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {489return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);490}491public fulfillContentWidget(detail: string): IMouseTargetContentWidget {492return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);493}494public fulfillScrollbar(position: Position): IMouseTargetScrollbar {495return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);496}497public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget {498return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);499}500}501502interface ResolvedHitTestRequest extends HitTestRequest {503readonly target: HTMLElement;504}505506const EMPTY_CONTENT_AFTER_LINES: IMouseTargetContentEmptyData = { isAfterLines: true };507508function createEmptyContentDataInLines(horizontalDistanceToText: number): IMouseTargetContentEmptyData {509return {510isAfterLines: false,511horizontalDistanceToText: horizontalDistanceToText512};513}514515export class MouseTargetFactory {516517private readonly _context: ViewContext;518private readonly _viewHelper: IPointerHandlerHelper;519520constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) {521this._context = context;522this._viewHelper = viewHelper;523}524525public mouseTargetIsWidget(e: EditorMouseEvent): boolean {526const t = <Element>e.target;527const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);528529// Is it a content widget?530if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {531return true;532}533534// Is it an overlay widget?535if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {536return true;537}538539return false;540}541542public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null): IMouseTarget {543const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);544const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);545try {546const r = MouseTargetFactory._createMouseTarget(ctx, request);547548if (r.type === MouseTargetType.CONTENT_TEXT) {549// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.550if (ctx.stickyTabStops && r.position !== null) {551const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);552const range = EditorRange.fromPositions(position, position).plusRange(r.range);553return request.fulfillContentText(position, range, r.detail);554}555}556557// console.log(MouseTarget.toString(r));558return r;559} catch (err) {560// console.log(err);561return request.fulfillUnknown();562}563}564565private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget {566567// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);568569if (request.target === null) {570// No target571return request.fulfillUnknown();572}573574// we know for a fact that request.target is not null575const resolvedRequest = <ResolvedHitTestRequest>request;576577let result: IMouseTarget | null = null;578579if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {580// We only render dom nodes inside the overflow guard or in the overflowing content widgets581result = result || request.fulfillUnknown();582}583584result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);585result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);586result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);587result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);588result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);589result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);590result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);591result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);592result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);593result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);594595return (result || request.fulfillUnknown());596}597598private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {599// Is it a content widget?600if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {601const widgetId = ctx.findAttribute(request.target, 'widgetId');602if (widgetId) {603return request.fulfillContentWidget(widgetId);604} else {605return request.fulfillUnknown();606}607}608return null;609}610611private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {612// Is it an overlay widget?613if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {614const widgetId = ctx.findAttribute(request.target, 'widgetId');615if (widgetId) {616return request.fulfillOverlayWidget(widgetId);617} else {618return request.fulfillUnknown();619}620}621return null;622}623624private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {625626if (request.target) {627// Check if we've hit a painted cursor628const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;629630for (const d of lastViewCursorsRenderData) {631632if (request.target === d.domNode) {633return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });634}635}636}637638if (request.isInContentArea) {639// Edge has a bug when hit-testing the exact position of a cursor,640// instead of returning the correct dom node, it returns the641// first or last rendered view line dom node, therefore help it out642// and first check if we are on top of a cursor643644const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;645const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;646const mouseVerticalOffset = request.mouseVerticalOffset;647648for (const d of lastViewCursorsRenderData) {649650if (mouseContentHorizontalOffset < d.contentLeft) {651// mouse position is to the left of the cursor652continue;653}654if (mouseContentHorizontalOffset > d.contentLeft + d.width) {655// mouse position is to the right of the cursor656continue;657}658659const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);660661if (662cursorVerticalOffset <= mouseVerticalOffset663&& mouseVerticalOffset <= cursorVerticalOffset + d.height664) {665return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });666}667}668}669670return null;671}672673private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {674const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);675if (viewZoneData) {676const mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE);677return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);678}679680return null;681}682683private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {684// Is it the textarea?685if (ElementPath.isTextArea(request.targetPath)) {686if (ctx.lastRenderData.lastTextareaPosition) {687return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });688}689return request.fulfillTextarea();690}691return null;692}693694private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {695if (request.isInMarginArea) {696const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);697const pos = res.range.getStartPosition();698let offset = Math.abs(request.relativePos.x);699const detail: Mutable<IMouseTargetMarginData> = {700isAfterLines: res.isAfterLines,701glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,702glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,703lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,704offsetX: offset705};706707offset -= ctx.layoutInfo.glyphMarginLeft;708709if (offset <= ctx.layoutInfo.glyphMarginWidth) {710// On the glyph margin711const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());712const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);713detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];714return request.fulfillMargin(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail);715}716offset -= ctx.layoutInfo.glyphMarginWidth;717718if (offset <= ctx.layoutInfo.lineNumbersWidth) {719// On the line numbers720return request.fulfillMargin(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail);721}722offset -= ctx.layoutInfo.lineNumbersWidth;723724// On the line decorations725return request.fulfillMargin(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail);726}727return null;728}729730private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {731if (!ElementPath.isChildOfViewLines(request.targetPath)) {732return null;733}734735if (ctx.isInTopPadding(request.mouseVerticalOffset)) {736return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);737}738739// Check if it is below any lines and any view zones740if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {741// This most likely indicates it happened after the last view-line742const lineCount = ctx.viewModel.getLineCount();743const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);744return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);745}746747// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)748// See https://github.com/microsoft/vscode/issues/46942749if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {750const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);751const lineLength = ctx.viewModel.getLineLength(lineNumber);752const lineWidth = ctx.getLineWidth(lineNumber);753if (lineLength === 0) {754const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);755return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);756}757758const isRtl = ctx.isRtl(lineNumber);759if (isRtl) {760if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {761const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);762const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));763return request.fulfillContentEmpty(pos, detail);764}765} else if (request.mouseContentHorizontalOffset >= lineWidth) {766const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);767const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));768return request.fulfillContentEmpty(pos, detail);769}770} else {771if (ctx.viewLinesGpu) {772const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);773if (ctx.viewModel.getLineLength(lineNumber) === 0) {774const lineWidth = ctx.getLineWidth(lineNumber);775const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);776return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);777}778779const lineWidth = ctx.getLineWidth(lineNumber);780const isRtl = ctx.isRtl(lineNumber);781if (isRtl) {782if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {783const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);784const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));785return request.fulfillContentEmpty(pos, detail);786}787} else if (request.mouseContentHorizontalOffset >= lineWidth) {788const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);789const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));790return request.fulfillContentEmpty(pos, detail);791}792793const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);794if (position) {795const detail: IMouseTargetContentTextData = {796injectedText: null,797mightBeForeignElement: false798};799return request.fulfillContentText(position, EditorRange.fromPositions(position, position), detail);800}801}802}803804// Do the hit test (if not already done)805const hitTestResult = request.hitTestResult.value;806807if (hitTestResult.type === HitTestResultType.Content) {808return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);809}810811// We didn't hit content...812if (request.wouldBenefitFromHitTestTargetSwitch) {813// We actually hit something different... Give it one last change by trying again with this new target814request.switchToHitTestTarget();815return this._createMouseTarget(ctx, request);816}817818// We have tried everything...819return request.fulfillUnknown();820}821822private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {823if (ElementPath.isChildOfMinimap(request.targetPath)) {824const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);825const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);826return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));827}828return null;829}830831private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {832if (ElementPath.isChildOfScrollableElement(request.targetPath)) {833if (request.target && request.target.nodeType === 1) {834const className = request.target.className;835if (className && /\b(slider|scrollbar)\b/.test(className)) {836const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);837const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);838return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));839}840}841}842return null;843}844845private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {846// Is it the overview ruler?847// Is it a child of the scrollable element?848if (ElementPath.isChildOfScrollableElement(request.targetPath)) {849const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);850const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);851return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));852}853854return null;855}856857public getMouseColumn(relativePos: CoordinatesRelativeToEditor): number {858const options = this._context.configuration.options;859const layoutInfo = options.get(EditorOption.layoutInfo);860const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;861return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth);862}863864public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number {865if (mouseContentHorizontalOffset < 0) {866return 1;867}868const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);869return (chars + 1);870}871872private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): IMouseTarget {873const lineNumber = pos.lineNumber;874const column = pos.column;875876const lineWidth = ctx.getLineWidth(lineNumber);877878if (request.mouseContentHorizontalOffset > lineWidth) {879const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);880return request.fulfillContentEmpty(pos, detail);881}882883const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);884885if (!visibleRange) {886return request.fulfillUnknown(pos);887}888889const columnHorizontalOffset = visibleRange.left;890891if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {892return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });893}894895// Let's define a, b, c and check if the offset is in between them...896interface OffsetColumn { offset: number; column: number }897898const points: OffsetColumn[] = [];899points.push({ offset: visibleRange.left, column: column });900if (column > 1) {901const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);902if (visibleRange) {903points.push({ offset: visibleRange.left, column: column - 1 });904}905}906const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);907if (column < lineMaxColumn) {908const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);909if (visibleRange) {910points.push({ offset: visibleRange.left, column: column + 1 });911}912}913914points.sort((a, b) => a.offset - b.offset);915916const mouseCoordinates = request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode));917const spanNodeClientRect = spanNode.getBoundingClientRect();918const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);919920let rng: EditorRange | null = null;921922for (let i = 1; i < points.length; i++) {923const prev = points[i - 1];924const curr = points[i];925if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {926rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);927928// See https://github.com/microsoft/vscode/issues/152819929// Due to the use of zwj, the browser's hit test result is skewed towards the left930// Here we try to correct that if the mouse horizontal offset is closer to the right than the left931932const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);933const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);934935pos = (936prevDelta < nextDelta937? new Position(lineNumber, prev.column)938: new Position(lineNumber, curr.column)939);940941break;942}943}944945return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });946}947948/**949* Most probably WebKit browsers and Edge950*/951private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {952953// In Chrome, especially on Linux it is possible to click between lines,954// so try to adjust the `hity` below so that it lands in the center of a line955const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);956const lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);957const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;958959const isBelowLastLine = (960lineNumber === ctx.viewModel.getLineCount()961&& request.mouseVerticalOffset > lineEndVerticalOffset962);963964if (!isBelowLastLine) {965const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);966let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);967968if (adjustedPageY <= request.editorPos.y) {969adjustedPageY = request.editorPos.y + 1;970}971if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {972adjustedPageY = request.editorPos.y + request.editorPos.height - 1;973}974975const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);976977const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));978if (r.type === HitTestResultType.Content) {979return r;980}981}982983// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)984return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));985}986987private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {988const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);989let range: Range;990if (shadowRoot) {991if (typeof (<any>shadowRoot).caretRangeFromPoint === 'undefined') {992range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);993} else {994range = (<any>shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY);995}996} else {997range = (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY);998}9991000if (!range || !range.startContainer) {1001return new UnknownHitTestResult();1002}10031004// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span1005const startContainer = range.startContainer;10061007if (startContainer.nodeType === startContainer.TEXT_NODE) {1008// startContainer is expected to be the token text1009const parent1 = startContainer.parentNode; // expected to be the token span1010const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span1011const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div1012const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;10131014if (parent3ClassName === ViewLine.CLASS_NAME) {1015return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>parent1, range.startOffset);1016} else {1017return new UnknownHitTestResult(<HTMLElement>startContainer.parentNode);1018}1019} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {1020// startContainer is expected to be the token span1021const parent1 = startContainer.parentNode; // expected to be the view line container span1022const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div1023const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;10241025if (parent2ClassName === ViewLine.CLASS_NAME) {1026return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>startContainer, (<HTMLElement>startContainer).textContent!.length);1027} else {1028return new UnknownHitTestResult(<HTMLElement>startContainer);1029}1030}10311032return new UnknownHitTestResult();1033}10341035/**1036* Most probably Gecko1037*/1038private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {1039const hitResult: { offsetNode: Node; offset: number } = (<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY);10401041if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {1042// offsetNode is expected to be the token text1043const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span1044const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span1045const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div1046const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;10471048if (parent3ClassName === ViewLine.CLASS_NAME) {1049return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode.parentNode, hitResult.offset);1050} else {1051return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode.parentNode);1052}1053}10541055// For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration1056// Some other times, it returns the `<span>` with the inline decoration1057if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {1058const parent1 = hitResult.offsetNode.parentNode;1059const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (<HTMLElement>parent1).className : null;1060const parent2 = parent1 ? parent1.parentNode : null;1061const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;10621063if (parent1ClassName === ViewLine.CLASS_NAME) {1064// it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration1065const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];1066if (tokenSpan) {1067return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>tokenSpan, 0);1068}1069} else if (parent2ClassName === ViewLine.CLASS_NAME) {1070// it returned the `<span>` with the inline decoration1071return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode, 0);1072}1073}10741075return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode);1076}10771078private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position {1079const lineContent = viewModel.getLineContent(position.lineNumber);1080const { tabSize } = viewModel.model.getOptions();1081const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest);1082if (newPosition !== -1) {1083return new Position(position.lineNumber, newPosition + 1);1084}1085return position;1086}10871088public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {10891090let result: HitTestResult = new UnknownHitTestResult();1091if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {1092result = this._doHitTestWithCaretRangeFromPoint(ctx, request);1093} else if ((<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint) {1094result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));1095}1096if (result.type === HitTestResultType.Content) {1097const injectedText = ctx.viewModel.getInjectedTextAt(result.position);10981099const normalizedPosition = ctx.viewModel.normalizePosition(result.position, PositionAffinity.None);1100if (injectedText || !normalizedPosition.equals(result.position)) {1101result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);1102}1103}1104return result;1105}1106}11071108function shadowCaretRangeFromPoint(shadowRoot: ShadowRoot, x: number, y: number): Range {1109const range = document.createRange();11101111// Get the element under the point1112let el: HTMLElement | null = (<any>shadowRoot).elementFromPoint(x, y);1113// 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.1114if (el?.hasChildNodes()) {1115// Get the last child of the element until its firstChild is a text node1116// This assumes that the pointer is on the right of the line, out of the tokens1117// and that we want to get the offset of the last token of the line1118while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {1119el = <HTMLElement>el.lastChild;1120}11211122// Grab its rect1123const rect = el.getBoundingClientRect();11241125// And its font (the computed shorthand font property might be empty, see #3217)1126const elWindow = dom.getWindow(el);1127const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');1128const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');1129const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');1130const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');1131const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');1132const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');1133const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;11341135// And also its txt content1136const text = el.innerText;11371138// Position the pixel cursor at the left of the element1139let pixelCursor = rect.left;1140let offset = 0;1141let step: number;11421143// If the point is on the right of the box put the cursor after the last character1144if (x > rect.left + rect.width) {1145offset = text.length;1146} else {1147const charWidthReader = CharWidthReader.getInstance();1148// Goes through all the characters of the innerText, and checks if the x of the point1149// belongs to the character.1150for (let i = 0; i < text.length + 1; i++) {1151// The step is half the width of the character1152step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;1153// Move to the center of the character1154pixelCursor += step;1155// If the x of the point is smaller that the position of the cursor, the point is over that character1156if (x < pixelCursor) {1157offset = i;1158break;1159}1160// Move between the current character and the next1161pixelCursor += step;1162}1163}11641165// Creates a range with the text node of the element and set the offset found1166range.setStart(el.firstChild!, offset);1167range.setEnd(el.firstChild!, offset);1168}11691170return range;1171}11721173class CharWidthReader {1174private static _INSTANCE: CharWidthReader | null = null;11751176public static getInstance(): CharWidthReader {1177if (!CharWidthReader._INSTANCE) {1178CharWidthReader._INSTANCE = new CharWidthReader();1179}1180return CharWidthReader._INSTANCE;1181}11821183private readonly _cache: { [cacheKey: string]: number };1184private readonly _canvas: HTMLCanvasElement;11851186private constructor() {1187this._cache = {};1188this._canvas = document.createElement('canvas');1189}11901191public getCharWidth(char: string, font: string): number {1192const cacheKey = char + font;1193if (this._cache[cacheKey]) {1194return this._cache[cacheKey];1195}11961197const context = this._canvas.getContext('2d')!;1198context.font = font;1199const metrics = context.measureText(char);1200const width = metrics.width;1201this._cache[cacheKey] = width;1202return width;1203}1204}120512061207