Skip to content

Commit 0c5078a

Browse files
authored
Merge pull request microsoft#208804 from microsoft/alexd/spiritual-bug
Avoid doing a hit test if the line is empty or if the mouse is after the line width
2 parents a375ea8 + 4284fcd commit 0c5078a

File tree

1 file changed

+73
-52
lines changed

1 file changed

+73
-52
lines changed

src/vs/editor/browser/controller/mouseTarget.ts

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/curs
2121
import { PositionAffinity } from 'vs/editor/common/model';
2222
import { InjectedText } from 'vs/editor/common/modelLineProjectionData';
2323
import { Mutable } from 'vs/base/common/types';
24+
import { Lazy } from 'vs/base/common/lazy';
2425

2526
const enum HitTestResultType {
2627
Unknown,
@@ -36,6 +37,9 @@ class UnknownHitTestResult {
3637

3738
class ContentHitTestResult {
3839
readonly type = HitTestResultType.Content;
40+
41+
get hitTarget(): HTMLElement { return this.spanNode; }
42+
3943
constructor(
4044
readonly position: Position,
4145
readonly spanNode: HTMLElement,
@@ -404,26 +408,53 @@ abstract class BareHitTestRequest {
404408

405409
class HitTestRequest extends BareHitTestRequest {
406410
private readonly _ctx: HitTestContext;
407-
public readonly target: HTMLElement | null;
408-
public readonly targetPath: Uint8Array;
411+
private readonly _eventTarget: HTMLElement | null;
412+
public readonly hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this));
413+
private _useHitTestTarget: boolean;
414+
private _targetPathCacheElement: HTMLElement | null = null;
415+
private _targetPathCacheValue: Uint8Array = new Uint8Array(0);
416+
417+
public get target(): HTMLElement | null {
418+
if (this._useHitTestTarget) {
419+
return this.hitTestResult.value.hitTarget;
420+
}
421+
return this._eventTarget;
422+
}
409423

410-
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null) {
424+
public get targetPath(): Uint8Array {
425+
if (this._targetPathCacheElement !== this.target) {
426+
this._targetPathCacheElement = this.target;
427+
this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode);
428+
}
429+
return this._targetPathCacheValue;
430+
}
431+
432+
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, eventTarget: HTMLElement | null) {
411433
super(ctx, editorPos, pos, relativePos);
412434
this._ctx = ctx;
435+
this._eventTarget = eventTarget;
413436

414-
if (target) {
415-
this.target = target;
416-
this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode);
417-
} else {
418-
this.target = null;
419-
this.targetPath = new Uint8Array(0);
420-
}
437+
// If no event target is passed in, we will use the hit test target
438+
const hasEventTarget = Boolean(this._eventTarget);
439+
this._useHitTestTarget = !hasEventTarget;
421440
}
422441

423442
public override toString(): string {
424443
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}`;
425444
}
426445

446+
public get wouldBenefitFromHitTestTargetSwitch(): boolean {
447+
return (
448+
!this._useHitTestTarget
449+
&& this.hitTestResult.value.hitTarget !== null
450+
&& this.target !== this.hitTestResult.value.hitTarget
451+
);
452+
}
453+
454+
public switchToHitTestTarget(): void {
455+
this._useHitTestTarget = true;
456+
}
457+
427458
private _getMouseColumn(position: Position | null = null): number {
428459
if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {
429460
// Most likely, the line contains foreign decorations...
@@ -459,10 +490,6 @@ class HitTestRequest extends BareHitTestRequest {
459490
public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget {
460491
return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
461492
}
462-
463-
public withTarget(target: HTMLElement | null): HitTestRequest {
464-
return new HitTestRequest(this._ctx, this.editorPos, this.pos, this.relativePos, target);
465-
}
466493
}
467494

468495
interface ResolvedHitTestRequest extends HitTestRequest {
@@ -509,7 +536,7 @@ export class MouseTargetFactory {
509536
const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
510537
const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
511538
try {
512-
const r = MouseTargetFactory._createMouseTarget(ctx, request, false);
539+
const r = MouseTargetFactory._createMouseTarget(ctx, request);
513540

514541
if (r.type === MouseTargetType.CONTENT_TEXT) {
515542
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
@@ -528,24 +555,13 @@ export class MouseTargetFactory {
528555
}
529556
}
530557

531-
private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): IMouseTarget {
558+
private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget {
532559

533560
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
534561

535-
// First ensure the request has a target
536562
if (request.target === null) {
537-
if (domHitTestExecuted) {
538-
// Still no target... and we have already executed hit test...
539-
return request.fulfillUnknown();
540-
}
541-
542-
const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
543-
544-
if (hitTestResult.type === HitTestResultType.Content) {
545-
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
546-
}
547-
548-
return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
563+
// No target
564+
return request.fulfillUnknown();
549565
}
550566

551567
// we know for a fact that request.target is not null
@@ -566,7 +582,7 @@ export class MouseTargetFactory {
566582
result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
567583
result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
568584
result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
569-
result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted);
585+
result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);
570586
result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
571587

572588
return (result || request.fulfillUnknown());
@@ -704,7 +720,7 @@ export class MouseTargetFactory {
704720
return null;
705721
}
706722

707-
private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest, domHitTestExecuted: boolean): IMouseTarget | null {
723+
private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
708724
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
709725
return null;
710726
}
@@ -721,36 +737,41 @@ export class MouseTargetFactory {
721737
return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
722738
}
723739

724-
if (domHitTestExecuted) {
725-
// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
726-
// See https://github.com/microsoft/vscode/issues/46942
727-
if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
728-
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
729-
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
730-
const lineWidth = ctx.getLineWidth(lineNumber);
731-
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
732-
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
733-
}
734-
740+
// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
741+
// See https://github.com/microsoft/vscode/issues/46942
742+
if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
743+
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
744+
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
735745
const lineWidth = ctx.getLineWidth(lineNumber);
736-
if (request.mouseContentHorizontalOffset >= lineWidth) {
737-
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
738-
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
739-
return request.fulfillContentEmpty(pos, detail);
740-
}
746+
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
747+
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
741748
}
742749

743-
// We have already executed hit test...
744-
return request.fulfillUnknown();
750+
const lineWidth = ctx.getLineWidth(lineNumber);
751+
if (request.mouseContentHorizontalOffset >= lineWidth) {
752+
// TODO: This is wrong for RTL
753+
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
754+
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
755+
return request.fulfillContentEmpty(pos, detail);
756+
}
745757
}
746758

747-
const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
759+
// Do the hit test (if not already done)
760+
const hitTestResult = request.hitTestResult.value;
748761

749762
if (hitTestResult.type === HitTestResultType.Content) {
750763
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
751764
}
752765

753-
return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
766+
// We didn't hit content...
767+
if (request.wouldBenefitFromHitTestTargetSwitch) {
768+
// We actually hit something different... Give it one last change by trying again with this new target
769+
request.switchToHitTestTarget();
770+
return this._createMouseTarget(ctx, request);
771+
}
772+
773+
// We have tried everything...
774+
return request.fulfillUnknown();
754775
}
755776

756777
private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null {
@@ -1019,7 +1040,7 @@ export class MouseTargetFactory {
10191040
return position;
10201041
}
10211042

1022-
private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
1043+
public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {
10231044

10241045
let result: HitTestResult = new UnknownHitTestResult();
10251046
if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {

0 commit comments

Comments
 (0)