Skip to content

Commit bc5d1b1

Browse files
committed
Reveal multiple cursors if they all fit in the viewport (microsoft#6995)
1 parent f71ee8c commit bc5d1b1

6 files changed

Lines changed: 137 additions & 73 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export class TextAreaHandler extends ViewPart {
259259
this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
260260
'keyboard',
261261
new Range(lineNumber, column, lineNumber, column),
262+
null,
262263
viewEvents.VerticalRevealType.Simple,
263264
true,
264265
ScrollType.Immediate

src/vs/editor/browser/viewParts/lines/viewLines.ts

Lines changed: 121 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/v
1212
import { DomReadingContext, ViewLine, ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLine';
1313
import { Position } from 'vs/editor/common/core/position';
1414
import { Range } from 'vs/editor/common/core/range';
15+
import { Selection } from 'vs/editor/common/core/selection';
1516
import { ScrollType } from 'vs/editor/common/editorCommon';
1617
import { IViewLines, LineVisibleRanges, VisibleRanges, HorizontalPosition } from 'vs/editor/common/view/renderingContext';
1718
import { ViewContext } from 'vs/editor/common/view/viewContext';
@@ -38,25 +39,49 @@ class LastRenderedData {
3839
}
3940
}
4041

41-
class HorizontalRevealRequest {
42-
43-
public readonly lineNumber: number;
44-
public readonly startColumn: number;
45-
public readonly endColumn: number;
46-
public readonly startScrollTop: number;
47-
public readonly stopScrollTop: number;
48-
public readonly scrollType: ScrollType;
42+
class HorizontalRevealRangeRequest {
43+
public readonly type = 'range';
44+
public readonly minLineNumber: number;
45+
public readonly maxLineNumber: number;
46+
47+
constructor(
48+
public readonly lineNumber: number,
49+
public readonly startColumn: number,
50+
public readonly endColumn: number,
51+
public readonly startScrollTop: number,
52+
public readonly stopScrollTop: number,
53+
public readonly scrollType: ScrollType
54+
) {
55+
this.minLineNumber = lineNumber;
56+
this.maxLineNumber = lineNumber;
57+
}
58+
}
4959

50-
constructor(lineNumber: number, startColumn: number, endColumn: number, startScrollTop: number, stopScrollTop: number, scrollType: ScrollType) {
51-
this.lineNumber = lineNumber;
52-
this.startColumn = startColumn;
53-
this.endColumn = endColumn;
54-
this.startScrollTop = startScrollTop;
55-
this.stopScrollTop = stopScrollTop;
56-
this.scrollType = scrollType;
60+
class HorizontalRevealSelectionsRequest {
61+
public readonly type = 'selections';
62+
public readonly minLineNumber: number;
63+
public readonly maxLineNumber: number;
64+
65+
constructor(
66+
public readonly selections: Selection[],
67+
public readonly startScrollTop: number,
68+
public readonly stopScrollTop: number,
69+
public readonly scrollType: ScrollType
70+
) {
71+
let minLineNumber = selections[0].startLineNumber;
72+
let maxLineNumber = selections[0].endLineNumber;
73+
for (let i = 1, len = selections.length; i < len; i++) {
74+
const selection = selections[i];
75+
minLineNumber = Math.min(minLineNumber, selection.startLineNumber);
76+
maxLineNumber = Math.max(maxLineNumber, selection.endLineNumber);
77+
}
78+
this.minLineNumber = minLineNumber;
79+
this.maxLineNumber = maxLineNumber;
5780
}
5881
}
5982

83+
type HorizontalRevealRequest = HorizontalRevealRangeRequest | HorizontalRevealSelectionsRequest;
84+
6085
export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>, IViewLines {
6186
/**
6287
* Adds this amount of pixels to the right of lines (no-one wants to type near the edge of the viewport)
@@ -221,21 +246,28 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
221246
public onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean {
222247
// Using the future viewport here in order to handle multiple
223248
// incoming reveal range requests that might all desire to be animated
224-
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.source, e.range, e.verticalType);
249+
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.source, e.range, e.selections, e.verticalType);
250+
251+
if (desiredScrollTop === -1) {
252+
// marker to abort the reveal range request
253+
return false;
254+
}
225255

226256
// validate the new desired scroll top
227257
let newScrollPosition = this._context.viewLayout.validateScrollPosition({ scrollTop: desiredScrollTop });
228258

229259
if (e.revealHorizontal) {
230-
if (e.range.startLineNumber !== e.range.endLineNumber) {
260+
if (e.range && e.range.startLineNumber !== e.range.endLineNumber) {
231261
// Two or more lines? => scroll to base (That's how you see most of the two lines)
232262
newScrollPosition = {
233263
scrollTop: newScrollPosition.scrollTop,
234264
scrollLeft: 0
235265
};
236-
} else {
266+
} else if (e.range) {
237267
// We don't necessarily know the horizontal offset of this range since the line might not be in the view...
238-
this._horizontalRevealRequest = new HorizontalRevealRequest(e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
268+
this._horizontalRevealRequest = new HorizontalRevealRangeRequest(e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
269+
} else if (e.selections && e.selections.length > 0) {
270+
this._horizontalRevealRequest = new HorizontalRevealSelectionsRequest(e.selections, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
239271
}
240272
} else {
241273
this._horizontalRevealRequest = null;
@@ -501,37 +533,34 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
501533
// - it might change `scrollWidth` and `scrollLeft`
502534
if (this._horizontalRevealRequest) {
503535

504-
const revealLineNumber = this._horizontalRevealRequest.lineNumber;
505-
const revealStartColumn = this._horizontalRevealRequest.startColumn;
506-
const revealEndColumn = this._horizontalRevealRequest.endColumn;
507-
const scrollType = this._horizontalRevealRequest.scrollType;
536+
const horizontalRevealRequest = this._horizontalRevealRequest;
508537

509538
// Check that we have the line that contains the horizontal range in the viewport
510-
if (viewportData.startLineNumber <= revealLineNumber && revealLineNumber <= viewportData.endLineNumber) {
539+
if (viewportData.startLineNumber <= horizontalRevealRequest.minLineNumber && horizontalRevealRequest.maxLineNumber <= viewportData.endLineNumber) {
511540

512541
this._horizontalRevealRequest = null;
513542

514543
// allow `visibleRangesForRange2` to work
515544
this.onDidRender();
516545

517546
// compute new scroll position
518-
const newScrollLeft = this._computeScrollLeftToRevealRange(revealLineNumber, revealStartColumn, revealEndColumn);
519-
520-
const isViewportWrapping = this._isViewportWrapping;
521-
if (!isViewportWrapping) {
522-
// ensure `scrollWidth` is large enough
523-
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
524-
}
525-
526-
// set `scrollLeft`
527-
if (scrollType === ScrollType.Smooth) {
528-
this._context.viewLayout.setScrollPositionSmooth({
529-
scrollLeft: newScrollLeft.scrollLeft
530-
});
531-
} else {
532-
this._context.viewLayout.setScrollPositionNow({
533-
scrollLeft: newScrollLeft.scrollLeft
534-
});
547+
const newScrollLeft = this._computeScrollLeftToReveal(horizontalRevealRequest);
548+
549+
if (newScrollLeft) {
550+
if (!this._isViewportWrapping) {
551+
// ensure `scrollWidth` is large enough
552+
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
553+
}
554+
// set `scrollLeft`
555+
if (horizontalRevealRequest.scrollType === ScrollType.Smooth) {
556+
this._context.viewLayout.setScrollPositionSmooth({
557+
scrollLeft: newScrollLeft.scrollLeft
558+
});
559+
} else {
560+
this._context.viewLayout.setScrollPositionNow({
561+
scrollLeft: newScrollLeft.scrollLeft
562+
});
563+
}
535564
}
536565
}
537566
}
@@ -560,16 +589,33 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
560589
}
561590
}
562591

563-
private _computeScrollTopToRevealRange(viewport: Viewport, source: string, range: Range, verticalType: viewEvents.VerticalRevealType): number {
592+
private _computeScrollTopToRevealRange(viewport: Viewport, source: string, range: Range | null, selections: Selection[] | null, verticalType: viewEvents.VerticalRevealType): number {
564593
const viewportStartY = viewport.top;
565594
const viewportHeight = viewport.height;
566595
const viewportEndY = viewportStartY + viewportHeight;
596+
let boxIsSingleRange: boolean;
567597
let boxStartY: number;
568598
let boxEndY: number;
569599

570600
// Have a box that includes one extra line height (for the horizontal scrollbar)
571-
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
572-
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
601+
if (selections && selections.length > 0) {
602+
let minLineNumber = selections[0].startLineNumber;
603+
let maxLineNumber = selections[0].endLineNumber;
604+
for (let i = 1, len = selections.length; i < len; i++) {
605+
const selection = selections[i];
606+
minLineNumber = Math.min(minLineNumber, selection.startLineNumber);
607+
maxLineNumber = Math.max(maxLineNumber, selection.endLineNumber);
608+
}
609+
boxIsSingleRange = false;
610+
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(minLineNumber);
611+
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(maxLineNumber) + this._lineHeight;
612+
} else if (range) {
613+
boxIsSingleRange = true;
614+
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
615+
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
616+
} else {
617+
return -1;
618+
}
573619

574620
const shouldIgnoreScrollOff = source === 'mouse' && this._cursorSurroundingLinesStyle === 'default';
575621

@@ -588,6 +634,10 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
588634

589635
if (boxEndY - boxStartY > viewportHeight) {
590636
// the box is larger than the viewport ... scroll to its top
637+
if (!boxIsSingleRange) {
638+
// do not reveal multiple cursors if there are more than fit the viewport
639+
return -1;
640+
}
591641
newScrollTop = boxStartY;
592642
} else if (verticalType === viewEvents.VerticalRevealType.NearTop || verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport) {
593643
if (verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
@@ -618,44 +668,50 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
618668
return newScrollTop;
619669
}
620670

621-
private _computeScrollLeftToRevealRange(lineNumber: number, startColumn: number, endColumn: number): { scrollLeft: number; maxHorizontalOffset: number; } {
622-
623-
let maxHorizontalOffset = 0;
671+
private _computeScrollLeftToReveal(horizontalRevealRequest: HorizontalRevealRequest): { scrollLeft: number; maxHorizontalOffset: number; } | null {
624672

625673
const viewport = this._context.viewLayout.getCurrentViewport();
626674
const viewportStartX = viewport.left;
627675
const viewportEndX = viewportStartX + viewport.width;
628676

629-
const visibleRanges = this._visibleRangesForLineRange(lineNumber, startColumn, endColumn);
630677
let boxStartX = Constants.MAX_SAFE_SMALL_INTEGER;
631678
let boxEndX = 0;
632-
633-
if (!visibleRanges) {
634-
// Unknown
635-
return {
636-
scrollLeft: viewportStartX,
637-
maxHorizontalOffset: maxHorizontalOffset
638-
};
639-
}
640-
641-
for (const visibleRange of visibleRanges.ranges) {
642-
if (visibleRange.left < boxStartX) {
643-
boxStartX = visibleRange.left;
679+
if (horizontalRevealRequest.type === 'range') {
680+
const visibleRanges = this._visibleRangesForLineRange(horizontalRevealRequest.lineNumber, horizontalRevealRequest.startColumn, horizontalRevealRequest.endColumn);
681+
if (!visibleRanges) {
682+
return null;
683+
}
684+
for (const visibleRange of visibleRanges.ranges) {
685+
boxStartX = Math.min(boxStartX, visibleRange.left);
686+
boxEndX = Math.max(boxEndX, visibleRange.left + visibleRange.width);
644687
}
645-
if (visibleRange.left + visibleRange.width > boxEndX) {
646-
boxEndX = visibleRange.left + visibleRange.width;
688+
} else {
689+
for (const selection of horizontalRevealRequest.selections) {
690+
if (selection.startLineNumber !== selection.endLineNumber) {
691+
return null;
692+
}
693+
const visibleRanges = this._visibleRangesForLineRange(selection.startLineNumber, selection.startColumn, selection.endColumn);
694+
if (!visibleRanges) {
695+
return null;
696+
}
697+
for (const visibleRange of visibleRanges.ranges) {
698+
boxStartX = Math.min(boxStartX, visibleRange.left);
699+
boxEndX = Math.max(boxEndX, visibleRange.left + visibleRange.width);
700+
}
647701
}
648702
}
649703

650-
maxHorizontalOffset = boxEndX;
651-
652704
boxStartX = Math.max(0, boxStartX - ViewLines.HORIZONTAL_EXTRA_PX);
653705
boxEndX += this._revealHorizontalRightPadding;
654706

707+
if (horizontalRevealRequest.type === 'selections' && boxEndX - boxStartX > viewport.width) {
708+
return null;
709+
}
710+
655711
const newScrollLeft = this._computeMinimumScrolling(viewportStartX, viewportEndX, boxStartX, boxEndX);
656712
return {
657713
scrollLeft: newScrollLeft,
658-
maxHorizontalOffset: maxHorizontalOffset
714+
maxHorizontalOffset: boxEndX
659715
};
660716
}
661717

src/vs/editor/browser/viewParts/minimap/minimap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,7 @@ export class Minimap extends ViewPart implements IMinimapModel {
10121012
this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
10131013
'mouse',
10141014
new Range(lineNumber, 1, lineNumber, 1),
1015+
null,
10151016
viewEvents.VerticalRevealType.Center,
10161017
false,
10171018
ScrollType.Smooth

src/vs/editor/browser/widget/codeEditorWidget.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
550550
const validatedModelRange = this._modelData.model.validateRange(modelRange);
551551
const viewRange = this._modelData.viewModel.coordinatesConverter.convertModelRangeToViewRange(validatedModelRange);
552552

553-
this._modelData.cursor.emitCursorRevealRange('api', viewRange, verticalType, revealHorizontal, scrollType);
553+
this._modelData.cursor.emitCursorRevealRange('api', viewRange, null, verticalType, revealHorizontal, scrollType);
554554
}
555555

556556
public revealLine(lineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void {

src/vs/editor/common/controller/cursor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
323323
}
324324

325325
public revealRange(source: string, revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: editorCommon.ScrollType) {
326-
this.emitCursorRevealRange(source, viewRange, verticalType, revealHorizontal, scrollType);
326+
this.emitCursorRevealRange(source, viewRange, null, verticalType, revealHorizontal, scrollType);
327327
}
328328

329329
public scrollTo(desiredScrollTop: number): void {
@@ -594,19 +594,19 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
594594
}
595595
} else {
596596
if (viewPositions.length > 1) {
597-
// no revealing!
597+
this.emitCursorRevealRange(source, null, this._cursors.getViewSelections(), verticalType, revealHorizontal, scrollType);
598598
return;
599599
}
600600
}
601601

602602
const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column);
603-
this.emitCursorRevealRange(source, viewRange, verticalType, revealHorizontal, scrollType);
603+
this.emitCursorRevealRange(source, viewRange, null, verticalType, revealHorizontal, scrollType);
604604
}
605605

606-
public emitCursorRevealRange(source: string, viewRange: Range, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) {
606+
public emitCursorRevealRange(source: string, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) {
607607
try {
608608
const eventsCollector = this._beginEmit();
609-
eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, verticalType, revealHorizontal, scrollType));
609+
eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType));
610610
} finally {
611611
this._endEmit();
612612
}

src/vs/editor/common/view/viewEvents.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,12 @@ export class ViewRevealRangeRequestEvent {
205205
/**
206206
* Range to be reavealed.
207207
*/
208-
public readonly range: Range;
208+
public readonly range: Range | null;
209+
210+
/**
211+
* Selections to be revealed.
212+
*/
213+
public readonly selections: Selection[] | null;
209214

210215
public readonly verticalType: VerticalRevealType;
211216
/**
@@ -221,9 +226,10 @@ export class ViewRevealRangeRequestEvent {
221226
*/
222227
readonly source: string;
223228

224-
constructor(source: string, range: Range, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: ScrollType) {
229+
constructor(source: string, range: Range | null, selections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: ScrollType) {
225230
this.source = source;
226231
this.range = range;
232+
this.selections = selections;
227233
this.verticalType = verticalType;
228234
this.revealHorizontal = revealHorizontal;
229235
this.scrollType = scrollType;

0 commit comments

Comments
 (0)