Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/mouseTarget.ts
3294 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((<any>target).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 ? (<HTMLElement>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
return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(position), position, detail);
485
}
486
public fulfillContentText(position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText {
487
return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);
488
}
489
public fulfillContentEmpty(position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty {
490
return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);
491
}
492
public fulfillContentWidget(detail: string): IMouseTargetContentWidget {
493
return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);
494
}
495
public fulfillScrollbar(position: Position): IMouseTargetScrollbar {
496
return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);
497
}
498
public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget {
499
return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
500
}
501
}
502
503
interface ResolvedHitTestRequest extends HitTestRequest {
504
readonly target: HTMLElement;
505
}
506
507
const EMPTY_CONTENT_AFTER_LINES: IMouseTargetContentEmptyData = { isAfterLines: true };
508
509
function createEmptyContentDataInLines(horizontalDistanceToText: number): IMouseTargetContentEmptyData {
510
return {
511
isAfterLines: false,
512
horizontalDistanceToText: horizontalDistanceToText
513
};
514
}
515
516
export class MouseTargetFactory {
517
518
private readonly _context: ViewContext;
519
private readonly _viewHelper: IPointerHandlerHelper;
520
521
constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) {
522
this._context = context;
523
this._viewHelper = viewHelper;
524
}
525
526
public mouseTargetIsWidget(e: EditorMouseEvent): boolean {
527
const t = <Element>e.target;
528
const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);
529
530
// Is it a content widget?
531
if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {
532
return true;
533
}
534
535
// Is it an overlay widget?
536
if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {
537
return true;
538
}
539
540
return false;
541
}
542
543
public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null): IMouseTarget {
544
const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
545
const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
546
try {
547
const r = MouseTargetFactory._createMouseTarget(ctx, request);
548
549
if (r.type === MouseTargetType.CONTENT_TEXT) {
550
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
551
if (ctx.stickyTabStops && r.position !== null) {
552
const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);
553
const range = EditorRange.fromPositions(position, position).plusRange(r.range);
554
return request.fulfillContentText(position, range, r.detail);
555
}
556
}
557
558
// console.log(MouseTarget.toString(r));
559
return r;
560
} catch (err) {
561
// console.log(err);
562
return request.fulfillUnknown();
563
}
564
}
565
566
private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget {
567
568
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
569
570
if (request.target === null) {
571
// No target
572
return request.fulfillUnknown();
573
}
574
575
// we know for a fact that request.target is not null
576
const resolvedRequest = <ResolvedHitTestRequest>request;
577
578
let result: IMouseTarget | null = null;
579
580
if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
581
// We only render dom nodes inside the overflow guard or in the overflowing content widgets
582
result = result || request.fulfillUnknown();
583
}
584
585
result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);
586
result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);
587
result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);
588
result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);
589
result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);
590
result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
591
result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
592
result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
593
result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);
594
result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
595
596
return (result || request.fulfillUnknown());
597
}
598
599
private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
600
// Is it a content widget?
601
if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
602
const widgetId = ctx.findAttribute(request.target, 'widgetId');
603
if (widgetId) {
604
return request.fulfillContentWidget(widgetId);
605
} else {
606
return request.fulfillUnknown();
607
}
608
}
609
return null;
610
}
611
612
private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
613
// Is it an overlay widget?
614
if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
615
const widgetId = ctx.findAttribute(request.target, 'widgetId');
616
if (widgetId) {
617
return request.fulfillOverlayWidget(widgetId);
618
} else {
619
return request.fulfillUnknown();
620
}
621
}
622
return null;
623
}
624
625
private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
626
627
if (request.target) {
628
// Check if we've hit a painted cursor
629
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
630
631
for (const d of lastViewCursorsRenderData) {
632
633
if (request.target === d.domNode) {
634
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
635
}
636
}
637
}
638
639
if (request.isInContentArea) {
640
// Edge has a bug when hit-testing the exact position of a cursor,
641
// instead of returning the correct dom node, it returns the
642
// first or last rendered view line dom node, therefore help it out
643
// and first check if we are on top of a cursor
644
645
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
646
const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
647
const mouseVerticalOffset = request.mouseVerticalOffset;
648
649
for (const d of lastViewCursorsRenderData) {
650
651
if (mouseContentHorizontalOffset < d.contentLeft) {
652
// mouse position is to the left of the cursor
653
continue;
654
}
655
if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
656
// mouse position is to the right of the cursor
657
continue;
658
}
659
660
const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
661
662
if (
663
cursorVerticalOffset <= mouseVerticalOffset
664
&& mouseVerticalOffset <= cursorVerticalOffset + d.height
665
) {
666
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
667
}
668
}
669
}
670
671
return null;
672
}
673
674
private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
675
const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
676
if (viewZoneData) {
677
const mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE);
678
return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);
679
}
680
681
return null;
682
}
683
684
private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
685
// Is it the textarea?
686
if (ElementPath.isTextArea(request.targetPath)) {
687
if (ctx.lastRenderData.lastTextareaPosition) {
688
return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });
689
}
690
return request.fulfillTextarea();
691
}
692
return null;
693
}
694
695
private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
696
if (request.isInMarginArea) {
697
const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
698
const pos = res.range.getStartPosition();
699
let offset = Math.abs(request.relativePos.x);
700
const detail: Mutable<IMouseTargetMarginData> = {
701
isAfterLines: res.isAfterLines,
702
glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,
703
glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,
704
lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,
705
offsetX: offset
706
};
707
708
offset -= ctx.layoutInfo.glyphMarginLeft;
709
710
if (offset <= ctx.layoutInfo.glyphMarginWidth) {
711
// On the glyph margin
712
const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());
713
const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);
714
detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];
715
return request.fulfillMargin(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail);
716
}
717
offset -= ctx.layoutInfo.glyphMarginWidth;
718
719
if (offset <= ctx.layoutInfo.lineNumbersWidth) {
720
// On the line numbers
721
return request.fulfillMargin(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail);
722
}
723
offset -= ctx.layoutInfo.lineNumbersWidth;
724
725
// On the line decorations
726
return request.fulfillMargin(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail);
727
}
728
return null;
729
}
730
731
private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
732
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
733
return null;
734
}
735
736
if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
737
return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);
738
}
739
740
// Check if it is below any lines and any view zones
741
if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {
742
// This most likely indicates it happened after the last view-line
743
const lineCount = ctx.viewModel.getLineCount();
744
const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);
745
return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
746
}
747
748
// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
749
// See https://github.com/microsoft/vscode/issues/46942
750
if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
751
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
752
const lineLength = ctx.viewModel.getLineLength(lineNumber);
753
const lineWidth = ctx.getLineWidth(lineNumber);
754
if (lineLength === 0) {
755
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
756
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
757
}
758
759
const isRtl = ctx.isRtl(lineNumber);
760
if (isRtl) {
761
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
762
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
763
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
764
return request.fulfillContentEmpty(pos, detail);
765
}
766
} else if (request.mouseContentHorizontalOffset >= lineWidth) {
767
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
768
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
769
return request.fulfillContentEmpty(pos, detail);
770
}
771
} else {
772
if (ctx.viewLinesGpu) {
773
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
774
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
775
const lineWidth = ctx.getLineWidth(lineNumber);
776
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
777
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
778
}
779
780
const lineWidth = ctx.getLineWidth(lineNumber);
781
const isRtl = ctx.isRtl(lineNumber);
782
if (isRtl) {
783
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
784
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
785
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
786
return request.fulfillContentEmpty(pos, detail);
787
}
788
} else if (request.mouseContentHorizontalOffset >= lineWidth) {
789
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
790
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
791
return request.fulfillContentEmpty(pos, detail);
792
}
793
794
const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);
795
if (position) {
796
const detail: IMouseTargetContentTextData = {
797
injectedText: null,
798
mightBeForeignElement: false
799
};
800
return request.fulfillContentText(position, EditorRange.fromPositions(position, position), detail);
801
}
802
}
803
}
804
805
// Do the hit test (if not already done)
806
const hitTestResult = request.hitTestResult.value;
807
808
if (hitTestResult.type === HitTestResultType.Content) {
809
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
810
}
811
812
// We didn't hit content...
813
if (request.wouldBenefitFromHitTestTargetSwitch) {
814
// We actually hit something different... Give it one last change by trying again with this new target
815
request.switchToHitTestTarget();
816
return this._createMouseTarget(ctx, request);
817
}
818
819
// We have tried everything...
820
return request.fulfillUnknown();
821
}
822
823
private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
824
if (ElementPath.isChildOfMinimap(request.targetPath)) {
825
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
826
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
827
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
828
}
829
return null;
830
}
831
832
private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
833
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
834
if (request.target && request.target.nodeType === 1) {
835
const className = request.target.className;
836
if (className && /\b(slider|scrollbar)\b/.test(className)) {
837
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
838
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
839
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
840
}
841
}
842
}
843
return null;
844
}
845
846
private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
847
// Is it the overview ruler?
848
// Is it a child of the scrollable element?
849
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
850
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
851
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
852
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
853
}
854
855
return null;
856
}
857
858
public getMouseColumn(relativePos: CoordinatesRelativeToEditor): number {
859
const options = this._context.configuration.options;
860
const layoutInfo = options.get(EditorOption.layoutInfo);
861
const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;
862
return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth);
863
}
864
865
public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number {
866
if (mouseContentHorizontalOffset < 0) {
867
return 1;
868
}
869
const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
870
return (chars + 1);
871
}
872
873
private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): IMouseTarget {
874
const lineNumber = pos.lineNumber;
875
const column = pos.column;
876
877
const lineWidth = ctx.getLineWidth(lineNumber);
878
879
if (request.mouseContentHorizontalOffset > lineWidth) {
880
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
881
return request.fulfillContentEmpty(pos, detail);
882
}
883
884
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
885
886
if (!visibleRange) {
887
return request.fulfillUnknown(pos);
888
}
889
890
const columnHorizontalOffset = visibleRange.left;
891
892
if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {
893
return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });
894
}
895
896
// Let's define a, b, c and check if the offset is in between them...
897
interface OffsetColumn { offset: number; column: number }
898
899
const points: OffsetColumn[] = [];
900
points.push({ offset: visibleRange.left, column: column });
901
if (column > 1) {
902
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);
903
if (visibleRange) {
904
points.push({ offset: visibleRange.left, column: column - 1 });
905
}
906
}
907
const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);
908
if (column < lineMaxColumn) {
909
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);
910
if (visibleRange) {
911
points.push({ offset: visibleRange.left, column: column + 1 });
912
}
913
}
914
915
points.sort((a, b) => a.offset - b.offset);
916
917
const mouseCoordinates = request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode));
918
const spanNodeClientRect = spanNode.getBoundingClientRect();
919
const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
920
921
let rng: EditorRange | null = null;
922
923
for (let i = 1; i < points.length; i++) {
924
const prev = points[i - 1];
925
const curr = points[i];
926
if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {
927
rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);
928
929
// See https://github.com/microsoft/vscode/issues/152819
930
// Due to the use of zwj, the browser's hit test result is skewed towards the left
931
// Here we try to correct that if the mouse horizontal offset is closer to the right than the left
932
933
const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);
934
const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);
935
936
pos = (
937
prevDelta < nextDelta
938
? new Position(lineNumber, prev.column)
939
: new Position(lineNumber, curr.column)
940
);
941
942
break;
943
}
944
}
945
946
return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });
947
}
948
949
/**
950
* Most probably WebKit browsers and Edge
951
*/
952
private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
953
954
// In Chrome, especially on Linux it is possible to click between lines,
955
// so try to adjust the `hity` below so that it lands in the center of a line
956
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
957
const lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
958
const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;
959
960
const isBelowLastLine = (
961
lineNumber === ctx.viewModel.getLineCount()
962
&& request.mouseVerticalOffset > lineEndVerticalOffset
963
);
964
965
if (!isBelowLastLine) {
966
const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);
967
let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
968
969
if (adjustedPageY <= request.editorPos.y) {
970
adjustedPageY = request.editorPos.y + 1;
971
}
972
if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {
973
adjustedPageY = request.editorPos.y + request.editorPos.height - 1;
974
}
975
976
const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
977
978
const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
979
if (r.type === HitTestResultType.Content) {
980
return r;
981
}
982
}
983
984
// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
985
return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
986
}
987
988
private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {
989
const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);
990
let range: Range;
991
if (shadowRoot) {
992
if (typeof (<any>shadowRoot).caretRangeFromPoint === 'undefined') {
993
range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);
994
} else {
995
range = (<any>shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY);
996
}
997
} else {
998
range = (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY);
999
}
1000
1001
if (!range || !range.startContainer) {
1002
return new UnknownHitTestResult();
1003
}
1004
1005
// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
1006
const startContainer = range.startContainer;
1007
1008
if (startContainer.nodeType === startContainer.TEXT_NODE) {
1009
// startContainer is expected to be the token text
1010
const parent1 = startContainer.parentNode; // expected to be the token span
1011
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
1012
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
1013
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
1014
1015
if (parent3ClassName === ViewLine.CLASS_NAME) {
1016
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>parent1, range.startOffset);
1017
} else {
1018
return new UnknownHitTestResult(<HTMLElement>startContainer.parentNode);
1019
}
1020
} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
1021
// startContainer is expected to be the token span
1022
const parent1 = startContainer.parentNode; // expected to be the view line container span
1023
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
1024
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;
1025
1026
if (parent2ClassName === ViewLine.CLASS_NAME) {
1027
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>startContainer, (<HTMLElement>startContainer).textContent!.length);
1028
} else {
1029
return new UnknownHitTestResult(<HTMLElement>startContainer);
1030
}
1031
}
1032
1033
return new UnknownHitTestResult();
1034
}
1035
1036
/**
1037
* Most probably Gecko
1038
*/
1039
private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {
1040
const hitResult: { offsetNode: Node; offset: number } = (<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY);
1041
1042
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
1043
// offsetNode is expected to be the token text
1044
const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
1045
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
1046
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
1047
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
1048
1049
if (parent3ClassName === ViewLine.CLASS_NAME) {
1050
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode.parentNode, hitResult.offset);
1051
} else {
1052
return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode.parentNode);
1053
}
1054
}
1055
1056
// For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration
1057
// Some other times, it returns the `<span>` with the inline decoration
1058
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {
1059
const parent1 = hitResult.offsetNode.parentNode;
1060
const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (<HTMLElement>parent1).className : null;
1061
const parent2 = parent1 ? parent1.parentNode : null;
1062
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;
1063
1064
if (parent1ClassName === ViewLine.CLASS_NAME) {
1065
// it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration
1066
const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];
1067
if (tokenSpan) {
1068
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>tokenSpan, 0);
1069
}
1070
} else if (parent2ClassName === ViewLine.CLASS_NAME) {
1071
// it returned the `<span>` with the inline decoration
1072
return HitTestResult.createFromDOMInfo(ctx, <HTMLElement>hitResult.offsetNode, 0);
1073
}
1074
}
1075
1076
return new UnknownHitTestResult(<HTMLElement>hitResult.offsetNode);
1077
}
1078
1079
private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position {
1080
const lineContent = viewModel.getLineContent(position.lineNumber);
1081
const { tabSize } = viewModel.model.getOptions();
1082
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest);
1083
if (newPosition !== -1) {
1084
return new Position(position.lineNumber, newPosition + 1);
1085
}
1086
return position;
1087
}
1088
1089
public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
1090
1091
let result: HitTestResult = new UnknownHitTestResult();
1092
if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {
1093
result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
1094
} else if ((<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint) {
1095
result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
1096
}
1097
if (result.type === HitTestResultType.Content) {
1098
const injectedText = ctx.viewModel.getInjectedTextAt(result.position);
1099
1100
const normalizedPosition = ctx.viewModel.normalizePosition(result.position, PositionAffinity.None);
1101
if (injectedText || !normalizedPosition.equals(result.position)) {
1102
result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
1103
}
1104
}
1105
return result;
1106
}
1107
}
1108
1109
function shadowCaretRangeFromPoint(shadowRoot: ShadowRoot, x: number, y: number): Range {
1110
const range = document.createRange();
1111
1112
// Get the element under the point
1113
let el: HTMLElement | null = (<any>shadowRoot).elementFromPoint(x, y);
1114
// 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.
1115
if (el?.hasChildNodes()) {
1116
// Get the last child of the element until its firstChild is a text node
1117
// This assumes that the pointer is on the right of the line, out of the tokens
1118
// and that we want to get the offset of the last token of the line
1119
while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {
1120
el = <HTMLElement>el.lastChild;
1121
}
1122
1123
// Grab its rect
1124
const rect = el.getBoundingClientRect();
1125
1126
// And its font (the computed shorthand font property might be empty, see #3217)
1127
const elWindow = dom.getWindow(el);
1128
const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');
1129
const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');
1130
const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');
1131
const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');
1132
const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');
1133
const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');
1134
const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;
1135
1136
// And also its txt content
1137
const text = el.innerText;
1138
1139
// Position the pixel cursor at the left of the element
1140
let pixelCursor = rect.left;
1141
let offset = 0;
1142
let step: number;
1143
1144
// If the point is on the right of the box put the cursor after the last character
1145
if (x > rect.left + rect.width) {
1146
offset = text.length;
1147
} else {
1148
const charWidthReader = CharWidthReader.getInstance();
1149
// Goes through all the characters of the innerText, and checks if the x of the point
1150
// belongs to the character.
1151
for (let i = 0; i < text.length + 1; i++) {
1152
// The step is half the width of the character
1153
step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;
1154
// Move to the center of the character
1155
pixelCursor += step;
1156
// If the x of the point is smaller that the position of the cursor, the point is over that character
1157
if (x < pixelCursor) {
1158
offset = i;
1159
break;
1160
}
1161
// Move between the current character and the next
1162
pixelCursor += step;
1163
}
1164
}
1165
1166
// Creates a range with the text node of the element and set the offset found
1167
range.setStart(el.firstChild!, offset);
1168
range.setEnd(el.firstChild!, offset);
1169
}
1170
1171
return range;
1172
}
1173
1174
class CharWidthReader {
1175
private static _INSTANCE: CharWidthReader | null = null;
1176
1177
public static getInstance(): CharWidthReader {
1178
if (!CharWidthReader._INSTANCE) {
1179
CharWidthReader._INSTANCE = new CharWidthReader();
1180
}
1181
return CharWidthReader._INSTANCE;
1182
}
1183
1184
private readonly _cache: { [cacheKey: string]: number };
1185
private readonly _canvas: HTMLCanvasElement;
1186
1187
private constructor() {
1188
this._cache = {};
1189
this._canvas = document.createElement('canvas');
1190
}
1191
1192
public getCharWidth(char: string, font: string): number {
1193
const cacheKey = char + font;
1194
if (this._cache[cacheKey]) {
1195
return this._cache[cacheKey];
1196
}
1197
1198
const context = this._canvas.getContext('2d')!;
1199
context.font = font;
1200
const metrics = context.measureText(char);
1201
const width = metrics.width;
1202
this._cache[cacheKey] = width;
1203
return width;
1204
}
1205
}
1206
1207