Skip to content

Commit 83d7b8e

Browse files
authored
Merge pull request microsoft#161378 from microsoft/alexd/editor-drag-scrolling
Reimplement how dragging auto-scrolls in the editor
2 parents 869fd7b + 3f09008 commit 83d7b8e

File tree

10 files changed

+442
-232
lines changed

10 files changed

+442
-232
lines changed

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

Lines changed: 169 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
import * as dom from 'vs/base/browser/dom';
77
import { StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
8-
import { TimeoutTimer } from 'vs/base/common/async';
98
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
109
import * as platform from 'vs/base/common/platform';
1110
import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget';
12-
import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser';
13-
import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom';
11+
import { IMouseTarget, IMouseTargetOutsideEditor, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser';
12+
import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor, PageCoordinates } from 'vs/editor/browser/editorDom';
1413
import { ViewController } from 'vs/editor/browser/view/viewController';
1514
import { EditorZoom } from 'vs/editor/common/config/editorZoom';
1615
import { Position } from 'vs/editor/common/core/position';
@@ -20,6 +19,7 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
2019
import * as viewEvents from 'vs/editor/common/viewEvents';
2120
import { ViewEventHandler } from 'vs/editor/common/viewEventHandler';
2221
import { EditorOption } from 'vs/editor/common/config/editorOptions';
22+
import { NavigationCommandRevealType } from 'vs/editor/browser/coreCommands';
2323

2424
export interface IPointerHandlerHelper {
2525
viewDomNode: HTMLElement;
@@ -34,6 +34,11 @@ export interface IPointerHandlerHelper {
3434
*/
3535
getLastRenderData(): PointerHandlerLastRenderData;
3636

37+
/**
38+
* Render right now
39+
*/
40+
renderNow(): void;
41+
3742
shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean;
3843
shouldSuppressMouseDownOnWidget(widgetId: string): boolean;
3944

@@ -69,6 +74,7 @@ export class MouseHandler extends ViewEventHandler {
6974
this._context,
7075
this.viewController,
7176
this.viewHelper,
77+
this.mouseTargetFactory,
7278
(e, testEventTarget) => this._createMouseTarget(e, testEventTarget),
7379
(e) => this._getMouseColumn(e)
7480
));
@@ -177,10 +183,6 @@ export class MouseHandler extends ViewEventHandler {
177183
public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
178184
return false;
179185
}
180-
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
181-
this._mouseDownOperation.onScrollChanged();
182-
return false;
183-
}
184186
// --- end event handlers
185187

186188
public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null {
@@ -313,36 +315,36 @@ export class MouseHandler extends ViewEventHandler {
313315

314316
class MouseDownOperation extends Disposable {
315317

316-
private readonly _context: ViewContext;
317-
private readonly _viewController: ViewController;
318-
private readonly _viewHelper: IPointerHandlerHelper;
319318
private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget;
320319
private readonly _getMouseColumn: (e: EditorMouseEvent) => number;
321320

322321
private readonly _mouseMoveMonitor: GlobalEditorPointerMoveMonitor;
323-
private readonly _onScrollTimeout: TimeoutTimer;
322+
private readonly _topBottomDragScrolling: TopBottomDragScrolling;
324323
private readonly _mouseState: MouseDownState;
325324

326325
private _currentSelection: Selection;
327326
private _isActive: boolean;
328327
private _lastMouseEvent: EditorMouseEvent | null;
329328

330329
constructor(
331-
context: ViewContext,
332-
viewController: ViewController,
333-
viewHelper: IPointerHandlerHelper,
330+
private readonly _context: ViewContext,
331+
private readonly _viewController: ViewController,
332+
private readonly _viewHelper: IPointerHandlerHelper,
333+
private readonly _mouseTargetFactory: MouseTargetFactory,
334334
createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget,
335335
getMouseColumn: (e: EditorMouseEvent) => number
336336
) {
337337
super();
338-
this._context = context;
339-
this._viewController = viewController;
340-
this._viewHelper = viewHelper;
341338
this._createMouseTarget = createMouseTarget;
342339
this._getMouseColumn = getMouseColumn;
343340

344341
this._mouseMoveMonitor = this._register(new GlobalEditorPointerMoveMonitor(this._viewHelper.viewDomNode));
345-
this._onScrollTimeout = this._register(new TimeoutTimer());
342+
this._topBottomDragScrolling = this._register(new TopBottomDragScrolling(
343+
this._context,
344+
this._viewHelper,
345+
this._mouseTargetFactory,
346+
(position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType)
347+
));
346348
this._mouseState = new MouseDownState();
347349

348350
this._currentSelection = new Selection(1, 1, 1, 1);
@@ -374,7 +376,12 @@ class MouseDownOperation extends Disposable {
374376
target: position
375377
});
376378
} else {
377-
this._dispatchMouse(position, true);
379+
if (position.type === MouseTargetType.OUTSIDE_EDITOR) {
380+
this._topBottomDragScrolling.start(position, e);
381+
} else {
382+
this._topBottomDragScrolling.stop();
383+
this._dispatchMouse(position, true, NavigationCommandRevealType.Minimal);
384+
}
378385
}
379386
}
380387

@@ -436,7 +443,7 @@ class MouseDownOperation extends Disposable {
436443
}
437444

438445
this._mouseState.isDragAndDrop = false;
439-
this._dispatchMouse(position, e.shiftKey);
446+
this._dispatchMouse(position, e.shiftKey, NavigationCommandRevealType.Minimal);
440447

441448
if (!this._isActive) {
442449
this._isActive = true;
@@ -452,7 +459,7 @@ class MouseDownOperation extends Disposable {
452459

453460
private _stop(): void {
454461
this._isActive = false;
455-
this._onScrollTimeout.cancel();
462+
this._topBottomDragScrolling.stop();
456463
}
457464

458465
public onHeightChanged(): void {
@@ -463,27 +470,6 @@ class MouseDownOperation extends Disposable {
463470
this._mouseMoveMonitor.stopMonitoring();
464471
}
465472

466-
public onScrollChanged(): void {
467-
if (!this._isActive) {
468-
return;
469-
}
470-
this._onScrollTimeout.setIfNotSet(() => {
471-
if (!this._lastMouseEvent) {
472-
return;
473-
}
474-
const position = this._findMousePosition(this._lastMouseEvent, false);
475-
if (!position) {
476-
// Ignoring because position is unknown
477-
return;
478-
}
479-
if (this._mouseState.isDragAndDrop) {
480-
// Ignoring because users are dragging the text
481-
return;
482-
}
483-
this._dispatchMouse(position, true);
484-
}, 10);
485-
}
486-
487473
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): void {
488474
this._currentSelection = e.selections[0];
489475
}
@@ -496,41 +482,45 @@ class MouseDownOperation extends Disposable {
496482
const mouseColumn = this._getMouseColumn(e);
497483

498484
if (e.posy < editorContent.y) {
499-
const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0);
485+
const outsideDistance = editorContent.y - e.posy;
486+
const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - outsideDistance, 0);
500487
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
501488
if (viewZoneData) {
502489
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
503490
if (newPosition) {
504-
return MouseTarget.createOutsideEditor(mouseColumn, newPosition);
491+
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'above', outsideDistance);
505492
}
506493
}
507494

508495
const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
509-
return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1));
496+
return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1), 'above', outsideDistance);
510497
}
511498

512499
if (e.posy > editorContent.y + editorContent.height) {
500+
const outsideDistance = e.posy - editorContent.y - editorContent.height;
513501
const verticalOffset = viewLayout.getCurrentScrollTop() + e.relativePos.y;
514502
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
515503
if (viewZoneData) {
516504
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
517505
if (newPosition) {
518-
return MouseTarget.createOutsideEditor(mouseColumn, newPosition);
506+
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'below', outsideDistance);
519507
}
520508
}
521509

522510
const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
523-
return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)));
511+
return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)), 'below', outsideDistance);
524512
}
525513

526514
const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y);
527515

528516
if (e.posx < editorContent.x) {
529-
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1));
517+
const outsideDistance = editorContent.x - e.posx;
518+
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1), 'left', outsideDistance);
530519
}
531520

532521
if (e.posx > editorContent.x + editorContent.width) {
533-
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)));
522+
const outsideDistance = e.posx - editorContent.x - editorContent.width;
523+
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance);
534524
}
535525

536526
return null;
@@ -574,14 +564,15 @@ class MouseDownOperation extends Disposable {
574564
return null;
575565
}
576566

577-
private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean): void {
567+
private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType): void {
578568
if (!position.position) {
579569
return;
580570
}
581571
this._viewController.dispatchMouse({
582572
position: position.position,
583573
mouseColumn: position.mouseColumn,
584574
startedOnLineNumbers: this._mouseState.startedOnLineNumbers,
575+
revealType,
585576

586577
inSelectionMode: inSelectionMode,
587578
mouseDownCount: this._mouseState.count,
@@ -598,6 +589,134 @@ class MouseDownOperation extends Disposable {
598589
}
599590
}
600591

592+
class TopBottomDragScrolling extends Disposable {
593+
594+
private _operation: TopBottomDragScrollingOperation | null;
595+
596+
constructor(
597+
private readonly _context: ViewContext,
598+
private readonly _viewHelper: IPointerHandlerHelper,
599+
private readonly _mouseTargetFactory: MouseTargetFactory,
600+
private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void,
601+
) {
602+
super();
603+
this._operation = null;
604+
}
605+
606+
public override dispose(): void {
607+
super.dispose();
608+
this.stop();
609+
}
610+
611+
public start(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void {
612+
if (this._operation) {
613+
this._operation.setPosition(position, mouseEvent);
614+
} else {
615+
this._operation = new TopBottomDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent);
616+
}
617+
}
618+
619+
public stop(): void {
620+
if (this._operation) {
621+
this._operation.dispose();
622+
this._operation = null;
623+
}
624+
}
625+
}
626+
627+
class TopBottomDragScrollingOperation extends Disposable {
628+
629+
private _position: IMouseTargetOutsideEditor;
630+
private _mouseEvent: EditorMouseEvent;
631+
private _lastTime: number;
632+
private _animationFrameDisposable: IDisposable;
633+
634+
constructor(
635+
private readonly _context: ViewContext,
636+
private readonly _viewHelper: IPointerHandlerHelper,
637+
private readonly _mouseTargetFactory: MouseTargetFactory,
638+
private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void,
639+
position: IMouseTargetOutsideEditor,
640+
mouseEvent: EditorMouseEvent
641+
) {
642+
super();
643+
this._position = position;
644+
this._mouseEvent = mouseEvent;
645+
this._lastTime = Date.now();
646+
this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(() => this._execute());
647+
}
648+
649+
public override dispose(): void {
650+
this._animationFrameDisposable.dispose();
651+
}
652+
653+
public setPosition(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void {
654+
this._position = position;
655+
this._mouseEvent = mouseEvent;
656+
}
657+
658+
/**
659+
* update internal state and return elapsed ms since last time
660+
*/
661+
private _tick(): number {
662+
const now = Date.now();
663+
const elapsed = now - this._lastTime;
664+
this._lastTime = now;
665+
return elapsed;
666+
}
667+
668+
/**
669+
* get the number of lines per second to auto-scroll
670+
*/
671+
private _getScrollSpeed(): number {
672+
const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight);
673+
const viewportInLines = this._context.configuration.options.get(EditorOption.layoutInfo).height / lineHeight;
674+
const outsideDistanceInLines = this._position.outsideDistance / lineHeight;
675+
676+
if (outsideDistanceInLines <= 1.5) {
677+
return Math.max(30, viewportInLines * (1 + outsideDistanceInLines));
678+
}
679+
if (outsideDistanceInLines <= 3) {
680+
return Math.max(60, viewportInLines * (2 + outsideDistanceInLines));
681+
}
682+
return Math.max(200, viewportInLines * (7 + outsideDistanceInLines));
683+
}
684+
685+
private _execute(): void {
686+
const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight);
687+
const scrollSpeedInLines = this._getScrollSpeed();
688+
const elapsed = this._tick();
689+
const scrollInPixels = scrollSpeedInLines * (elapsed / 1000) * lineHeight;
690+
const scrollValue = (this._position.outsidePosition === 'above' ? -scrollInPixels : scrollInPixels);
691+
692+
this._context.viewModel.viewLayout.deltaScrollNow(0, scrollValue);
693+
this._viewHelper.renderNow();
694+
695+
const viewportData = this._context.viewLayout.getLinesViewportData();
696+
const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber);
697+
698+
// First, try to find a position that matches the horizontal position of the mouse
699+
let mouseTarget: IMouseTarget;
700+
{
701+
const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode);
702+
const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight;
703+
const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1);
704+
const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos);
705+
mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
706+
}
707+
if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) {
708+
if (this._position.outsidePosition === 'above') {
709+
mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance);
710+
} else {
711+
mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, this._context.viewModel.getLineMaxColumn(edgeLineNumber)), 'below', this._position.outsideDistance);
712+
}
713+
}
714+
715+
this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None);
716+
this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(() => this._execute());
717+
}
718+
}
719+
601720
class MouseDownState {
602721

603722
private static readonly CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ export class MouseTarget {
9999
public static createOverlayWidget(element: Element | null, mouseColumn: number, detail: string): IMouseTargetOverlayWidget {
100100
return { type: MouseTargetType.OVERLAY_WIDGET, element, mouseColumn, position: null, range: null, detail };
101101
}
102-
public static createOutsideEditor(mouseColumn: number, position: Position): IMouseTargetOutsideEditor {
103-
return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position) };
102+
public static createOutsideEditor(mouseColumn: number, position: Position, outsidePosition: 'above' | 'below' | 'left' | 'right', outsideDistance: number): IMouseTargetOutsideEditor {
103+
return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance };
104104
}
105105

106106
private static _typeToString(type: MouseTargetType): string {

0 commit comments

Comments
 (0)