Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/mouseTarget.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IPointerHandlerHelper } from './mouseHandler.js';
7
import { IMouseTargetContentEmptyData, IMouseTargetMarginData, IMouseTarget, IMouseTargetContentEmpty, IMouseTargetContentText, IMouseTargetContentWidget, IMouseTargetMargin, IMouseTargetOutsideEditor, IMouseTargetOverlayWidget, IMouseTargetScrollbar, IMouseTargetTextarea, IMouseTargetUnknown, IMouseTargetViewZone, IMouseTargetContentTextData, IMouseTargetViewZoneData, MouseTargetType } from '../editorBrowser.js';
8
import { ClientCoordinates, EditorMouseEvent, EditorPagePosition, PageCoordinates, CoordinatesRelativeToEditor } from '../editorDom.js';
9
import { PartFingerprint, PartFingerprints } from '../view/viewPart.js';
10
import { ViewLine } from '../viewParts/viewLines/viewLine.js';
11
import { IViewCursorRenderData } from '../viewParts/viewCursors/viewCursor.js';
12
import { EditorLayoutInfo, EditorOption } from '../../common/config/editorOptions.js';
13
import { Position } from '../../common/core/position.js';
14
import { Range as EditorRange } from '../../common/core/range.js';
15
import { HorizontalPosition } from '../view/renderingContext.js';
16
import { ViewContext } from '../../common/viewModel/viewContext.js';
17
import { IViewModel } from '../../common/viewModel.js';
18
import { CursorColumns } from '../../common/core/cursorColumns.js';
19
import * as dom from '../../../base/browser/dom.js';
20
import { AtomicTabMoveOperations, Direction } from '../../common/cursor/cursorAtomicMoveOperations.js';
21
import { PositionAffinity, TextDirection } from '../../common/model.js';
22
import { InjectedText } from '../../common/modelLineProjectionData.js';
23
import { Mutable } from '../../../base/common/types.js';
24
import { Lazy } from '../../../base/common/lazy.js';
25
import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js';
26
27
const enum HitTestResultType {
28
Unknown,
29
Content,
30
}
31
32
class UnknownHitTestResult {
33
readonly type = HitTestResultType.Unknown;
34
constructor(
35
readonly hitTarget: HTMLElement | null = null
36
) { }
37
}
38
39
class ContentHitTestResult {
40
readonly type = HitTestResultType.Content;
41
42
get hitTarget(): HTMLElement { return this.spanNode; }
43
44
constructor(
45
readonly position: Position,
46
readonly spanNode: HTMLElement,
47
readonly injectedText: InjectedText | null,
48
) { }
49
}
50
51
type HitTestResult = UnknownHitTestResult | ContentHitTestResult;
52
53
namespace HitTestResult {
54
export function createFromDOMInfo(ctx: HitTestContext, spanNode: HTMLElement, offset: number): HitTestResult {
55
const position = ctx.getPositionFromDOMInfo(spanNode, offset);
56
if (position) {
57
return new ContentHitTestResult(position, spanNode, null);
58
}
59
return new UnknownHitTestResult(spanNode);
60
}
61
}
62
63
export class PointerHandlerLastRenderData {
64
constructor(
65
public readonly lastViewCursorsRenderData: IViewCursorRenderData[],
66
public readonly lastTextareaPosition: Position | null
67
) { }
68
}
69
70
export class MouseTarget {
71
72
private static _deduceRage(position: Position): EditorRange;
73
private static _deduceRage(position: Position, range: EditorRange | null): EditorRange;
74
private static _deduceRage(position: Position | null): EditorRange | null;
75
private static _deduceRage(position: Position | null, range: EditorRange | null = null): EditorRange | null {
76
if (!range && position) {
77
return new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
78
}
79
return range ?? null;
80
}
81
public static createUnknown(element: HTMLElement | null, mouseColumn: number, position: Position | null): IMouseTargetUnknown {
82
return { type: MouseTargetType.UNKNOWN, element, mouseColumn, position, range: this._deduceRage(position) };
83
}
84
public static createTextarea(element: HTMLElement | null, mouseColumn: number): IMouseTargetTextarea {
85
return { type: MouseTargetType.TEXTAREA, element, mouseColumn, position: null, range: null };
86
}
87
public 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 {
88
return { type, element, mouseColumn, position, range, detail };
89
}
90
public static createViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, element: HTMLElement | null, mouseColumn: number, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone {
91
return { type, element, mouseColumn, position, range: this._deduceRage(position), detail };
92
}
93
public static createContentText(element: HTMLElement | null, mouseColumn: number, position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {
94
return { type: MouseTargetType.CONTENT_TEXT, element, mouseColumn, position, range: this._deduceRage(position, range), detail };
95
}
96
public static createContentEmpty(element: HTMLElement | null, mouseColumn: number, position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {
97
return { type: MouseTargetType.CONTENT_EMPTY, element, mouseColumn, position, range: this._deduceRage(position), detail };
98
}
99
public static createContentWidget(element: HTMLElement | null, mouseColumn: number, detail: string): IMouseTargetContentWidget {
100
return { type: MouseTargetType.CONTENT_WIDGET, element, mouseColumn, position: null, range: null, detail };
101
}
102
public static createScrollbar(element: HTMLElement | null, mouseColumn: number, position: Position): IMouseTargetScrollbar {
103
return { type: MouseTargetType.SCROLLBAR, element, mouseColumn, position, range: this._deduceRage(position) };
104
}
105
public static createOverlayWidget(element: HTMLElement | null, mouseColumn: number, detail: string): IMouseTargetOverlayWidget {
106
return { type: MouseTargetType.OVERLAY_WIDGET, element, mouseColumn, position: null, range: null, detail };
107
}
108
public static createOutsideEditor(mouseColumn: number, position: Position, outsidePosition: 'above' | 'below' | 'left' | 'right', outsideDistance: number): IMouseTargetOutsideEditor {
109
return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance };
110
}
111
112
private static _typeToString(type: MouseTargetType): string {
113
if (type === MouseTargetType.TEXTAREA) {
114
return 'TEXTAREA';
115
}
116
if (type === MouseTargetType.GUTTER_GLYPH_MARGIN) {
117
return 'GUTTER_GLYPH_MARGIN';
118
}
119
if (type === MouseTargetType.GUTTER_LINE_NUMBERS) {
120
return 'GUTTER_LINE_NUMBERS';
121
}
122
if (type === MouseTargetType.GUTTER_LINE_DECORATIONS) {
123
return 'GUTTER_LINE_DECORATIONS';
124
}
125
if (type === MouseTargetType.GUTTER_VIEW_ZONE) {
126
return 'GUTTER_VIEW_ZONE';
127
}
128
if (type === MouseTargetType.CONTENT_TEXT) {
129
return 'CONTENT_TEXT';
130
}
131
if (type === MouseTargetType.CONTENT_EMPTY) {
132
return 'CONTENT_EMPTY';
133
}
134
if (type === MouseTargetType.CONTENT_VIEW_ZONE) {
135
return 'CONTENT_VIEW_ZONE';
136
}
137
if (type === MouseTargetType.CONTENT_WIDGET) {
138
return 'CONTENT_WIDGET';
139
}
140
if (type === MouseTargetType.OVERVIEW_RULER) {
141
return 'OVERVIEW_RULER';
142
}
143
if (type === MouseTargetType.SCROLLBAR) {
144
return 'SCROLLBAR';
145
}
146
if (type === MouseTargetType.OVERLAY_WIDGET) {
147
return 'OVERLAY_WIDGET';
148
}
149
return 'UNKNOWN';
150
}
151
152
public static toString(target: IMouseTarget): string {
153
return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify((target as unknown as Record<string, unknown>).detail);
154
}
155
}
156
157
class ElementPath {
158
159
public static isTextArea(path: Uint8Array): boolean {
160
return (
161
path.length === 2
162
&& path[0] === PartFingerprint.OverflowGuard
163
&& path[1] === PartFingerprint.TextArea
164
);
165
}
166
167
public static isChildOfViewLines(path: Uint8Array): boolean {
168
return (
169
path.length >= 4
170
&& path[0] === PartFingerprint.OverflowGuard
171
&& path[3] === PartFingerprint.ViewLines
172
);
173
}
174
175
public static isStrictChildOfViewLines(path: Uint8Array): boolean {
176
return (
177
path.length > 4
178
&& path[0] === PartFingerprint.OverflowGuard
179
&& path[3] === PartFingerprint.ViewLines
180
);
181
}
182
183
public static isChildOfScrollableElement(path: Uint8Array): boolean {
184
return (
185
path.length >= 2
186
&& path[0] === PartFingerprint.OverflowGuard
187
&& path[1] === PartFingerprint.ScrollableElement
188
);
189
}
190
191
public static isChildOfMinimap(path: Uint8Array): boolean {
192
return (
193
path.length >= 2
194
&& path[0] === PartFingerprint.OverflowGuard
195
&& path[1] === PartFingerprint.Minimap
196
);
197
}
198
199
public static isChildOfContentWidgets(path: Uint8Array): boolean {
200
return (
201
path.length >= 4
202
&& path[0] === PartFingerprint.OverflowGuard
203
&& path[3] === PartFingerprint.ContentWidgets
204
);
205
}
206
207
public static isChildOfOverflowGuard(path: Uint8Array): boolean {
208
return (
209
path.length >= 1
210
&& path[0] === PartFingerprint.OverflowGuard
211
);
212
}
213
214
public static isChildOfOverflowingContentWidgets(path: Uint8Array): boolean {
215
return (
216
path.length >= 1
217
&& path[0] === PartFingerprint.OverflowingContentWidgets
218
);
219
}
220
221
public static isChildOfOverlayWidgets(path: Uint8Array): boolean {
222
return (
223
path.length >= 2
224
&& path[0] === PartFingerprint.OverflowGuard
225
&& path[1] === PartFingerprint.OverlayWidgets
226
);
227
}
228
229
public static isChildOfOverflowingOverlayWidgets(path: Uint8Array): boolean {
230
return (
231
path.length >= 1
232
&& path[0] === PartFingerprint.OverflowingOverlayWidgets
233
);
234
}
235
}
236
237
export class HitTestContext {
238
239
public readonly viewModel: IViewModel;
240
public readonly layoutInfo: EditorLayoutInfo;
241
public readonly viewDomNode: HTMLElement;
242
public readonly viewLinesGpu: ViewLinesGpu | undefined;
243
public readonly lineHeight: number;
244
public readonly stickyTabStops: boolean;
245
public readonly typicalHalfwidthCharacterWidth: number;
246
public readonly lastRenderData: PointerHandlerLastRenderData;
247
248
private readonly _context: ViewContext;
249
private readonly _viewHelper: IPointerHandlerHelper;
250
251
constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastRenderData: PointerHandlerLastRenderData) {
252
this.viewModel = context.viewModel;
253
const options = context.configuration.options;
254
this.layoutInfo = options.get(EditorOption.layoutInfo);
255
this.viewDomNode = viewHelper.viewDomNode;
256
this.viewLinesGpu = viewHelper.viewLinesGpu;
257
this.lineHeight = options.get(EditorOption.lineHeight);
258
this.stickyTabStops = options.get(EditorOption.stickyTabStops);
259
this.typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
260
this.lastRenderData = lastRenderData;
261
this._context = context;
262
this._viewHelper = viewHelper;
263
}
264
265
public getZoneAtCoord(mouseVerticalOffset: number): IMouseTargetViewZoneData | null {
266
return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset);
267
}
268
269
public static getZoneAtCoord(context: ViewContext, mouseVerticalOffset: number): IMouseTargetViewZoneData | null {
270
// The target is either a view zone or the empty space after the last view-line
271
const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
272
273
if (viewZoneWhitespace) {
274
const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2;
275
const lineCount = context.viewModel.getLineCount();
276
let positionBefore: Position | null = null;
277
let position: Position | null;
278
let positionAfter: Position | null = null;
279
280
if (viewZoneWhitespace.afterLineNumber !== lineCount) {
281
// There are more lines after this view zone
282
positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);
283
}
284
if (viewZoneWhitespace.afterLineNumber > 0) {
285
// There are more lines above this view zone
286
positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.viewModel.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
287
}
288
289
if (positionAfter === null) {
290
position = positionBefore;
291
} else if (positionBefore === null) {
292
position = positionAfter;
293
} else if (mouseVerticalOffset < viewZoneMiddle) {
294
position = positionBefore;
295
} else {
296
position = positionAfter;
297
}
298
299
return {
300
viewZoneId: viewZoneWhitespace.id,
301
afterLineNumber: viewZoneWhitespace.afterLineNumber,
302
positionBefore: positionBefore,
303
positionAfter: positionAfter,
304
position: position!
305
};
306
}
307
return null;
308
}
309
310
public getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean } {
311
if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {
312
// Below the last line
313
const lineNumber = this._context.viewModel.getLineCount();
314
const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
315
return {
316
range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
317
isAfterLines: true
318
};
319
}
320
321
const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
322
const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
323
return {
324
range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),
325
isAfterLines: false
326
};
327
}
328
329
public getLineNumberAtVerticalOffset(mouseVerticalOffset: number): number {
330
return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
331
}
332
333
public isAfterLines(mouseVerticalOffset: number): boolean {
334
return this._context.viewLayout.isAfterLines(mouseVerticalOffset);
335
}
336
337
public isInTopPadding(mouseVerticalOffset: number): boolean {
338
return this._context.viewLayout.isInTopPadding(mouseVerticalOffset);
339
}
340
341
public isInBottomPadding(mouseVerticalOffset: number): boolean {
342
return this._context.viewLayout.isInBottomPadding(mouseVerticalOffset);
343
}
344
345
public getVerticalOffsetForLineNumber(lineNumber: number): number {
346
return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
347
}
348
349
public findAttribute(element: Element, attr: string): string | null {
350
return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);
351
}
352
353
private static _findAttribute(element: Element, attr: string, stopAt: Element): string | null {
354
while (element && element !== element.ownerDocument.body) {
355
if (element.hasAttribute && element.hasAttribute(attr)) {
356
return element.getAttribute(attr);
357
}
358
if (element === stopAt) {
359
return null;
360
}
361
element = <Element>element.parentNode;
362
}
363
return null;
364
}
365
366
public getLineWidth(lineNumber: number): number {
367
return this._viewHelper.getLineWidth(lineNumber);
368
}
369
370
public isRtl(lineNumber: number): boolean {
371
return this.viewModel.getTextDirection(lineNumber) === TextDirection.RTL;
372
373
}
374
375
public visibleRangeForPosition(lineNumber: number, column: number): HorizontalPosition | null {
376
return this._viewHelper.visibleRangeForPosition(lineNumber, column);
377
}
378
379
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position | null {
380
return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);
381
}
382
383
public getCurrentScrollTop(): number {
384
return this._context.viewLayout.getCurrentScrollTop();
385
}
386
387
public getCurrentScrollLeft(): number {
388
return this._context.viewLayout.getCurrentScrollLeft();
389
}
390
}
391
392
abstract class BareHitTestRequest {
393
394
public readonly editorPos: EditorPagePosition;
395
public readonly pos: PageCoordinates;
396
public readonly relativePos: CoordinatesRelativeToEditor;
397
public readonly mouseVerticalOffset: number;
398
public readonly isInMarginArea: boolean;
399
public readonly isInContentArea: boolean;
400
public readonly mouseContentHorizontalOffset: number;
401
402
protected readonly mouseColumn: number;
403
404
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor) {
405
this.editorPos = editorPos;
406
this.pos = pos;
407
this.relativePos = relativePos;
408
409
this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y);
410
this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft;
411
this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft);
412
this.isInContentArea = !this.isInMarginArea;
413
this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
414
}
415
}
416
417
class HitTestRequest extends BareHitTestRequest {
418
private readonly _ctx: HitTestContext;
419
private readonly _eventTarget: HTMLElement | null;
420
public readonly hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this));
421
private _useHitTestTarget: boolean;
422
private _targetPathCacheElement: HTMLElement | null = null;
423
private _targetPathCacheValue: Uint8Array = new Uint8Array(0);
424
425
public get target(): HTMLElement | null {
426
if (this._useHitTestTarget) {
427
return this.hitTestResult.value.hitTarget;
428
}
429
return this._eventTarget;
430
}
431
432
public get targetPath(): Uint8Array {
433
if (this._targetPathCacheElement !== this.target) {
434
this._targetPathCacheElement = this.target;
435
this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode);
436
}
437
return this._targetPathCacheValue;
438
}
439
440
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, eventTarget: HTMLElement | null) {
441
super(ctx, editorPos, pos, relativePos);
442
this._ctx = ctx;
443
this._eventTarget = eventTarget;
444
445
// If no event target is passed in, we will use the hit test target
446
const hasEventTarget = Boolean(this._eventTarget);
447
this._useHitTestTarget = !hasEventTarget;
448
}
449
450
public override toString(): string {
451
return `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}`;
452
}
453
454
public get wouldBenefitFromHitTestTargetSwitch(): boolean {
455
return (
456
!this._useHitTestTarget
457
&& this.hitTestResult.value.hitTarget !== null
458
&& this.target !== this.hitTestResult.value.hitTarget
459
);
460
}
461
462
public switchToHitTestTarget(): void {
463
this._useHitTestTarget = true;
464
}
465
466
private _getMouseColumn(position: Position | null = null): number {
467
if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {
468
// Most likely, the line contains foreign decorations...
469
return CursorColumns.visibleColumnFromColumn(this._ctx.viewModel.getLineContent(position.lineNumber), position.column, this._ctx.viewModel.model.getOptions().tabSize) + 1;
470
}
471
return this.mouseColumn;
472
}
473
474
public fulfillUnknown(position: Position | null = null): IMouseTargetUnknown {
475
return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position);
476
}
477
public fulfillTextarea(): IMouseTargetTextarea {
478
return MouseTarget.createTextarea(this.target, this._getMouseColumn());
479
}
480
public fulfillMargin(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMouseTargetMarginData): IMouseTargetMargin {
481
return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail);
482
}
483
public fulfillViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone {
484
// Always return the usual mouse column for a view zone.
485
return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(), position, detail);
486
}
487
public fulfillContentText(position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {
488
return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);
489
}
490
public fulfillContentEmpty(position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {
491
return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);
492
}
493
public fulfillContentWidget(detail: string): IMouseTargetContentWidget {
494
return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);
495
}
496
public fulfillScrollbar(position: Position): IMouseTargetScrollbar {
497
return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);
498
}
499
public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget {
500
return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
501
}
502
}
503
504
interface ResolvedHitTestRequest extends HitTestRequest {
505
readonly target: HTMLElement;
506
}
507
508
const EMPTY_CONTENT_AFTER_LINES: IMouseTargetContentEmptyData = { isAfterLines: true };
509
510
function createEmptyContentDataInLines(horizontalDistanceToText: number): IMouseTargetContentEmptyData {
511
return {
512
isAfterLines: false,
513
horizontalDistanceToText: horizontalDistanceToText
514
};
515
}
516
517
export class MouseTargetFactory {
518
519
private readonly _context: ViewContext;
520
private readonly _viewHelper: IPointerHandlerHelper;
521
522
constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) {
523
this._context = context;
524
this._viewHelper = viewHelper;
525
}
526
527
public mouseTargetIsWidget(e: EditorMouseEvent): boolean {
528
const t = <Element>e.target;
529
const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);
530
531
// Is it a content widget?
532
if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {
533
return true;
534
}
535
536
// Is it an overlay widget?
537
if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {
538
return true;
539
}
540
541
return false;
542
}
543
544
public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null): IMouseTarget {
545
const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
546
const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
547
try {
548
const r = MouseTargetFactory._createMouseTarget(ctx, request);
549
550
if (r.type === MouseTargetType.CONTENT_TEXT) {
551
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
552
if (ctx.stickyTabStops && r.position !== null) {
553
const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);
554
const range = EditorRange.fromPositions(position, position).plusRange(r.range);
555
return request.fulfillContentText(position, range, r.detail);
556
}
557
}
558
559
// console.log(MouseTarget.toString(r));
560
return r;
561
} catch (err) {
562
// console.log(err);
563
return request.fulfillUnknown();
564
}
565
}
566
567
private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget {
568
569
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
570
571
if (request.target === null) {
572
// No target
573
return request.fulfillUnknown();
574
}
575
576
// we know for a fact that request.target is not null
577
const resolvedRequest = <ResolvedHitTestRequest>request;
578
579
let result: IMouseTarget | null = null;
580
581
if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
582
// We only render dom nodes inside the overflow guard or in the overflowing content widgets
583
result = result || request.fulfillUnknown();
584
}
585
586
result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);
587
result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);
588
result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);
589
result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);
590
result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);
591
result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
592
result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
593
result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
594
result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);
595
result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
596
597
return (result || request.fulfillUnknown());
598
}
599
600
private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
601
// Is it a content widget?
602
if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
603
const widgetId = ctx.findAttribute(request.target, 'widgetId');
604
if (widgetId) {
605
return request.fulfillContentWidget(widgetId);
606
} else {
607
return request.fulfillUnknown();
608
}
609
}
610
return null;
611
}
612
613
private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
614
// Is it an overlay widget?
615
if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
616
const widgetId = ctx.findAttribute(request.target, 'widgetId');
617
if (widgetId) {
618
return request.fulfillOverlayWidget(widgetId);
619
} else {
620
return request.fulfillUnknown();
621
}
622
}
623
return null;
624
}
625
626
private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
627
628
if (request.target) {
629
// Check if we've hit a painted cursor
630
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
631
632
for (const d of lastViewCursorsRenderData) {
633
634
if (request.target === d.domNode) {
635
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
636
}
637
}
638
}
639
640
if (request.isInContentArea) {
641
// Edge has a bug when hit-testing the exact position of a cursor,
642
// instead of returning the correct dom node, it returns the
643
// first or last rendered view line dom node, therefore help it out
644
// and first check if we are on top of a cursor
645
646
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
647
const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
648
const mouseVerticalOffset = request.mouseVerticalOffset;
649
650
for (const d of lastViewCursorsRenderData) {
651
652
if (mouseContentHorizontalOffset < d.contentLeft) {
653
// mouse position is to the left of the cursor
654
continue;
655
}
656
if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
657
// mouse position is to the right of the cursor
658
continue;
659
}
660
661
const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
662
663
if (
664
cursorVerticalOffset <= mouseVerticalOffset
665
&& mouseVerticalOffset <= cursorVerticalOffset + d.height
666
) {
667
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
668
}
669
}
670
}
671
672
return null;
673
}
674
675
private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
676
const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
677
if (viewZoneData) {
678
const mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE);
679
return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);
680
}
681
682
return null;
683
}
684
685
private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
686
// Is it the textarea?
687
if (ElementPath.isTextArea(request.targetPath)) {
688
if (ctx.lastRenderData.lastTextareaPosition) {
689
return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });
690
}
691
return request.fulfillTextarea();
692
}
693
return null;
694
}
695
696
private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
697
if (request.isInMarginArea) {
698
const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
699
const pos = res.range.getStartPosition();
700
let offset = Math.abs(request.relativePos.x);
701
const detail: Mutable<IMouseTargetMarginData> = {
702
isAfterLines: res.isAfterLines,
703
glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,
704
glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,
705
lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,
706
offsetX: offset
707
};
708
709
offset -= ctx.layoutInfo.glyphMarginLeft;
710
711
if (offset <= ctx.layoutInfo.glyphMarginWidth) {
712
// On the glyph margin
713
const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());
714
const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);
715
detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];
716
return request.fulfillMargin(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail);
717
}
718
offset -= ctx.layoutInfo.glyphMarginWidth;
719
720
if (offset <= ctx.layoutInfo.lineNumbersWidth) {
721
// On the line numbers
722
return request.fulfillMargin(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail);
723
}
724
offset -= ctx.layoutInfo.lineNumbersWidth;
725
726
// On the line decorations
727
return request.fulfillMargin(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail);
728
}
729
return null;
730
}
731
732
private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
733
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
734
return null;
735
}
736
737
if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
738
return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);
739
}
740
741
// Check if it is below any lines and any view zones
742
if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {
743
// This most likely indicates it happened after the last view-line
744
const lineCount = ctx.viewModel.getLineCount();
745
const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);
746
return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
747
}
748
749
// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
750
// See https://github.com/microsoft/vscode/issues/46942
751
if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
752
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
753
const lineLength = ctx.viewModel.getLineLength(lineNumber);
754
const lineWidth = ctx.getLineWidth(lineNumber);
755
if (lineLength === 0) {
756
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
757
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
758
}
759
760
const isRtl = ctx.isRtl(lineNumber);
761
if (isRtl) {
762
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
763
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
764
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
765
return request.fulfillContentEmpty(pos, detail);
766
}
767
} else if (request.mouseContentHorizontalOffset >= lineWidth) {
768
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
769
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
770
return request.fulfillContentEmpty(pos, detail);
771
}
772
} else {
773
if (ctx.viewLinesGpu) {
774
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
775
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
776
const lineWidth = ctx.getLineWidth(lineNumber);
777
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
778
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
779
}
780
781
const lineWidth = ctx.getLineWidth(lineNumber);
782
const isRtl = ctx.isRtl(lineNumber);
783
if (isRtl) {
784
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
785
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
786
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
787
return request.fulfillContentEmpty(pos, detail);
788
}
789
} else if (request.mouseContentHorizontalOffset >= lineWidth) {
790
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
791
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
792
return request.fulfillContentEmpty(pos, detail);
793
}
794
795
const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);
796
if (position) {
797
const detail: IMouseTargetContentTextData = {
798
injectedText: null,
799
mightBeForeignElement: false
800
};
801
return request.fulfillContentText(position, EditorRange.fromPositions(position, position), detail);
802
}
803
}
804
}
805
806
// Do the hit test (if not already done)
807
const hitTestResult = request.hitTestResult.value;
808
809
if (hitTestResult.type === HitTestResultType.Content) {
810
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
811
}
812
813
// We didn't hit content...
814
if (request.wouldBenefitFromHitTestTargetSwitch) {
815
// We actually hit something different... Give it one last change by trying again with this new target
816
request.switchToHitTestTarget();
817
return this._createMouseTarget(ctx, request);
818
}
819
820
// We have tried everything...
821
return request.fulfillUnknown();
822
}
823
824
private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
825
if (ElementPath.isChildOfMinimap(request.targetPath)) {
826
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
827
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
828
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
829
}
830
return null;
831
}
832
833
private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
834
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
835
if (request.target && request.target.nodeType === 1) {
836
const className = request.target.className;
837
if (className && /\b(slider|scrollbar)\b/.test(className)) {
838
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
839
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
840
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
841
}
842
}
843
}
844
return null;
845
}
846
847
private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
848
// Is it the overview ruler?
849
// Is it a child of the scrollable element?
850
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
851
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
852
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
853
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
854
}
855
856
return null;
857
}
858
859
public getMouseColumn(relativePos: CoordinatesRelativeToEditor): number {
860
const options = this._context.configuration.options;
861
const layoutInfo = options.get(EditorOption.layoutInfo);
862
const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;
863
return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth);
864
}
865
866
public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number {
867
if (mouseContentHorizontalOffset < 0) {
868
return 1;
869
}
870
const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
871
return (chars + 1);
872
}
873
874
private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): IMouseTarget {
875
const lineNumber = pos.lineNumber;
876
const column = pos.column;
877
878
const lineWidth = ctx.getLineWidth(lineNumber);
879
880
if (request.mouseContentHorizontalOffset > lineWidth) {
881
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
882
return request.fulfillContentEmpty(pos, detail);
883
}
884
885
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
886
887
if (!visibleRange) {
888
return request.fulfillUnknown(pos);
889
}
890
891
const columnHorizontalOffset = visibleRange.left;
892
893
if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {
894
return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });
895
}
896
897
// Let's define a, b, c and check if the offset is in between them...
898
interface OffsetColumn { offset: number; column: number }
899
900
const points: OffsetColumn[] = [];
901
points.push({ offset: visibleRange.left, column: column });
902
if (column > 1) {
903
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);
904
if (visibleRange) {
905
points.push({ offset: visibleRange.left, column: column - 1 });
906
}
907
}
908
const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);
909
if (column < lineMaxColumn) {
910
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);
911
if (visibleRange) {
912
points.push({ offset: visibleRange.left, column: column + 1 });
913
}
914
}
915
916
points.sort((a, b) => a.offset - b.offset);
917
918
const mouseCoordinates = request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode));
919
const spanNodeClientRect = spanNode.getBoundingClientRect();
920
const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
921
922
let rng: EditorRange | null = null;
923
924
for (let i = 1; i < points.length; i++) {
925
const prev = points[i - 1];
926
const curr = points[i];
927
if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {
928
rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);
929
930
// See https://github.com/microsoft/vscode/issues/152819
931
// Due to the use of zwj, the browser's hit test result is skewed towards the left
932
// Here we try to correct that if the mouse horizontal offset is closer to the right than the left
933
934
const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);
935
const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);
936
937
pos = (
938
prevDelta < nextDelta
939
? new Position(lineNumber, prev.column)
940
: new Position(lineNumber, curr.column)
941
);
942
943
break;
944
}
945
}
946
947
return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });
948
}
949
950
/**
951
* Most probably WebKit browsers and Edge
952
*/
953
private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
954
955
// In Chrome, especially on Linux it is possible to click between lines,
956
// so try to adjust the `hity` below so that it lands in the center of a line
957
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
958
const lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
959
const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;
960
961
const isBelowLastLine = (
962
lineNumber === ctx.viewModel.getLineCount()
963
&& request.mouseVerticalOffset > lineEndVerticalOffset
964
);
965
966
if (!isBelowLastLine) {
967
const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);
968
let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
969
970
if (adjustedPageY <= request.editorPos.y) {
971
adjustedPageY = request.editorPos.y + 1;
972
}
973
if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {
974
adjustedPageY = request.editorPos.y + request.editorPos.height - 1;
975
}
976
977
const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
978
979
const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
980
if (r.type === HitTestResultType.Content) {
981
return r;
982
}
983
}
984
985
// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
986
return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
987
}
988
989
private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {
990
const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);
991
let range: Range;
992
if (shadowRoot) {
993
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
994
if (typeof (<any>shadowRoot).caretRangeFromPoint === 'undefined') {
995
range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);
996
} else {
997
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
998
range = (<any>shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY);
999
}
1000
} else {
1001
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
1002
range = (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY);
1003
}
1004
1005
if (!range || !range.startContainer) {
1006
return new UnknownHitTestResult();
1007
}
1008
1009
// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
1010
const startContainer = range.startContainer;
1011
1012
if (startContainer.nodeType === startContainer.TEXT_NODE) {
1013
// startContainer is expected to be the token text
1014
const parent1 = startContainer.parentNode; // expected to be the token span
1015
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
1016
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
1017
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
1018
1019
if (parent3ClassName === ViewLine.CLASS_NAME) {
1020
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>parent1, range.startOffset);
1021
} else {
1022
return new UnknownHitTestResult(<HTMLElement>startContainer.parentNode);
1023
}
1024
} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
1025
// startContainer is expected to be the token span
1026
const parent1 = startContainer.parentNode; // expected to be the view line container span
1027
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
1028
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;
1029
1030
if (parent2ClassName === ViewLine.CLASS_NAME) {
1031
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>startContainer, (<HTMLElement>startContainer).textContent.length);
1032
} else {
1033
return new UnknownHitTestResult(<HTMLElement>startContainer);
1034
}
1035
}
1036
1037
return new UnknownHitTestResult();
1038
}
1039
1040
/**
1041
* Most probably Gecko
1042
*/
1043
private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {
1044
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
1045
const hitResult: { offsetNode: Node; offset: number } = (<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY);
1046
1047
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
1048
// offsetNode is expected to be the token text
1049
const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
1050
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
1051
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
1052
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
1053
1054
if (parent3ClassName === ViewLine.CLASS_NAME) {
1055
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode.parentNode, hitResult.offset);
1056
} else {
1057
return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode.parentNode);
1058
}
1059
}
1060
1061
// For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration
1062
// Some other times, it returns the `<span>` with the inline decoration
1063
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {
1064
const parent1 = hitResult.offsetNode.parentNode;
1065
const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (<HTMLElement>parent1).className : null;
1066
const parent2 = parent1 ? parent1.parentNode : null;
1067
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;
1068
1069
if (parent1ClassName === ViewLine.CLASS_NAME) {
1070
// it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration
1071
const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];
1072
if (tokenSpan) {
1073
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>tokenSpan, 0);
1074
}
1075
} else if (parent2ClassName === ViewLine.CLASS_NAME) {
1076
// it returned the `<span>` with the inline decoration
1077
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode, 0);
1078
}
1079
}
1080
1081
return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode);
1082
}
1083
1084
private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position {
1085
const lineContent = viewModel.getLineContent(position.lineNumber);
1086
const { tabSize } = viewModel.model.getOptions();
1087
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest);
1088
if (newPosition !== -1) {
1089
return new Position(position.lineNumber, newPosition + 1);
1090
}
1091
return position;
1092
}
1093
1094
public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
1095
1096
let result: HitTestResult = new UnknownHitTestResult();
1097
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
1098
if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {
1099
result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
1100
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
1101
} else if ((<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint) {
1102
result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
1103
}
1104
if (result.type === HitTestResultType.Content) {
1105
const injectedText = ctx.viewModel.getInjectedTextAt(result.position);
1106
1107
const normalizedPosition = ctx.viewModel.normalizePosition(result.position, PositionAffinity.None);
1108
if (injectedText || !normalizedPosition.equals(result.position)) {
1109
result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
1110
}
1111
}
1112
return result;
1113
}
1114
}
1115
1116
function shadowCaretRangeFromPoint(shadowRoot: ShadowRoot, x: number, y: number): Range {
1117
const range = document.createRange();
1118
1119
// Get the element under the point
1120
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
1121
let el: HTMLElement | null = (<any>shadowRoot).elementFromPoint(x, y);
1122
// 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.
1123
if (el?.hasChildNodes()) {
1124
// Get the last child of the element until its firstChild is a text node
1125
// This assumes that the pointer is on the right of the line, out of the tokens
1126
// and that we want to get the offset of the last token of the line
1127
while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {
1128
el = <HTMLElement>el.lastChild;
1129
}
1130
1131
// Grab its rect
1132
const rect = el.getBoundingClientRect();
1133
1134
// And its font (the computed shorthand font property might be empty, see #3217)
1135
const elWindow = dom.getWindow(el);
1136
const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');
1137
const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');
1138
const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');
1139
const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');
1140
const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');
1141
const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');
1142
const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;
1143
1144
// And also its txt content
1145
const text = el.innerText;
1146
1147
// Position the pixel cursor at the left of the element
1148
let pixelCursor = rect.left;
1149
let offset = 0;
1150
let step: number;
1151
1152
// If the point is on the right of the box put the cursor after the last character
1153
if (x > rect.left + rect.width) {
1154
offset = text.length;
1155
} else {
1156
const charWidthReader = CharWidthReader.getInstance();
1157
// Goes through all the characters of the innerText, and checks if the x of the point
1158
// belongs to the character.
1159
for (let i = 0; i < text.length + 1; i++) {
1160
// The step is half the width of the character
1161
step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;
1162
// Move to the center of the character
1163
pixelCursor += step;
1164
// If the x of the point is smaller that the position of the cursor, the point is over that character
1165
if (x < pixelCursor) {
1166
offset = i;
1167
break;
1168
}
1169
// Move between the current character and the next
1170
pixelCursor += step;
1171
}
1172
}
1173
1174
// Creates a range with the text node of the element and set the offset found
1175
range.setStart(el.firstChild!, offset);
1176
range.setEnd(el.firstChild!, offset);
1177
}
1178
1179
return range;
1180
}
1181
1182
class CharWidthReader {
1183
private static _INSTANCE: CharWidthReader | null = null;
1184
1185
public static getInstance(): CharWidthReader {
1186
if (!CharWidthReader._INSTANCE) {
1187
CharWidthReader._INSTANCE = new CharWidthReader();
1188
}
1189
return CharWidthReader._INSTANCE;
1190
}
1191
1192
private readonly _cache: { [cacheKey: string]: number };
1193
private readonly _canvas: HTMLCanvasElement;
1194
1195
private constructor() {
1196
this._cache = {};
1197
this._canvas = document.createElement('canvas');
1198
}
1199
1200
public getCharWidth(char: string, font: string): number {
1201
const cacheKey = char + font;
1202
if (this._cache[cacheKey]) {
1203
return this._cache[cacheKey];
1204
}
1205
1206
const context = this._canvas.getContext('2d')!;
1207
context.font = font;
1208
const metrics = context.measureText(char);
1209
const width = metrics.width;
1210
this._cache[cacheKey] = width;
1211
return width;
1212
}
1213
}
1214
1215