Skip to content

Commit c37edea

Browse files
akbyrdalexdima
andauthored
Implement separate colors for primary and secondary cursors when multiple cursors are present (#181991)
* Add support for separate primary cursor color when multiple cursors are present - Does not change the existing behavior when there's a single cursor. editorCursor.foreground and background are still used. - Add editorCursor.multiple.primary.foreground and background theme colors for the primary cursor. Only used when multiple cursors exist. Fallback to editorCursor.foreground/background when theme colors aren't set. - Add editorCursor.multiple.secondary.foreground and `background theme colors for non-primary cursors. Only used when multiple cursors exist. Fallback to editorCursor.foreground/background when theme colors aren't set. Add cursor-primary and cursor-secondary html classes to target with cursor color styles. No new class is introduced in the single-cursor case. - Currently does not affect overview ruler colors. editorCursor.foreground is still used, even when multiple cursors are present. * Update overview ruler to use primary and secondary cursor colors - This maintains the existing handling for colors being undefined. However, each of these colors have defaults do I'm not sure if it's actually possible for them to be undefined * Fix formatting * Fix compilation errors * Fall back to the existing cursor colors (to avoid breaking existing themes) --------- Co-authored-by: Alex Dima <alexdima@microsoft.com>
1 parent 763ad71 commit c37edea

File tree

4 files changed

+108
-34
lines changed

4 files changed

+108
-34
lines changed

src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts

+42-17
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart';
1010
import { Position } from 'vs/editor/common/core/position';
1111
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
1212
import { TokenizationRegistry } from 'vs/editor/common/languages';
13-
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry';
13+
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry';
1414
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
1515
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
1616
import { EditorTheme } from 'vs/editor/common/editorTheme';
@@ -29,7 +29,9 @@ class Settings {
2929
public readonly borderColor: string | null;
3030

3131
public readonly hideCursor: boolean;
32-
public readonly cursorColor: string | null;
32+
public readonly cursorColorSingle: string | null;
33+
public readonly cursorColorPrimary: string | null;
34+
public readonly cursorColorSecondary: string | null;
3335

3436
public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark';
3537
public readonly backgroundColor: Color | null;
@@ -55,8 +57,12 @@ class Settings {
5557
this.borderColor = borderColor ? borderColor.toString() : null;
5658

5759
this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler);
58-
const cursorColor = theme.getColor(editorCursorForeground);
59-
this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
60+
const cursorColorSingle = theme.getColor(editorCursorForeground);
61+
this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null;
62+
const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground);
63+
this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null;
64+
const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground);
65+
this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null;
6066

6167
this.themeType = theme.type;
6268

@@ -189,7 +195,9 @@ class Settings {
189195
&& this.renderBorder === other.renderBorder
190196
&& this.borderColor === other.borderColor
191197
&& this.hideCursor === other.hideCursor
192-
&& this.cursorColor === other.cursorColor
198+
&& this.cursorColorSingle === other.cursorColorSingle
199+
&& this.cursorColorPrimary === other.cursorColorPrimary
200+
&& this.cursorColorSecondary === other.cursorColorSecondary
193201
&& this.themeType === other.themeType
194202
&& Color.equals(this.backgroundColor, other.backgroundColor)
195203
&& this.top === other.top
@@ -213,6 +221,11 @@ const enum OverviewRulerLane {
213221
Full = 7
214222
}
215223

224+
type Cursor = {
225+
position: Position;
226+
color: string | null;
227+
};
228+
216229
const enum ShouldRenderValue {
217230
NotNeeded = 0,
218231
Maybe = 1,
@@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart {
226239
private readonly _tokensColorTrackerListener: IDisposable;
227240
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
228241
private _settings!: Settings;
229-
private _cursorPositions: Position[];
242+
private _cursorPositions: Cursor[];
230243

231244
private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
232-
private _renderedCursorPositions: Position[] = [];
245+
private _renderedCursorPositions: Cursor[] = [];
233246

234247
constructor(context: ViewContext) {
235248
super(context);
@@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart {
249262
}
250263
});
251264

252-
this._cursorPositions = [new Position(1, 1)];
265+
this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }];
253266
}
254267

255268
public override dispose(): void {
@@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart {
298311
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
299312
this._cursorPositions = [];
300313
for (let i = 0, len = e.selections.length; i < len; i++) {
301-
this._cursorPositions[i] = e.selections[i].getPosition();
314+
let color = this._settings.cursorColorSingle;
315+
if (len > 1) {
316+
color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary;
317+
}
318+
this._cursorPositions.push({ position: e.selections[i].getPosition(), color });
302319
}
303-
this._cursorPositions.sort(Position.compare);
320+
this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position));
304321
return this._markRenderingIsMaybeNeeded();
305322
}
306323
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
@@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart {
352369
if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) {
353370
this._actualShouldRender = ShouldRenderValue.Needed;
354371
}
355-
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) {
372+
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) {
356373
this._actualShouldRender = ShouldRenderValue.Needed;
357374
}
358375
if (this._actualShouldRender === ShouldRenderValue.Maybe) {
@@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart {
443460
}
444461

445462
// Draw cursors
446-
if (!this._settings.hideCursor && this._settings.cursorColor) {
463+
if (!this._settings.hideCursor) {
447464
const cursorHeight = (2 * this._settings.pixelRatio) | 0;
448465
const halfCursorHeight = (cursorHeight / 2) | 0;
449466
const cursorX = this._settings.x[OverviewRulerLane.Full];
450467
const cursorW = this._settings.w[OverviewRulerLane.Full];
451-
canvasCtx.fillStyle = this._settings.cursorColor;
452468

453469
let prevY1 = -100;
454470
let prevY2 = -100;
471+
let prevColor: string | null = null;
455472
for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
456-
const cursor = this._cursorPositions[i];
473+
const color = this._cursorPositions[i].color;
474+
if (!color) {
475+
continue;
476+
}
477+
const cursor = this._cursorPositions[i].position;
457478

458479
let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0;
459480
if (yCenter < halfCursorHeight) {
@@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart {
464485
const y1 = yCenter - halfCursorHeight;
465486
const y2 = y1 + cursorHeight;
466487

467-
if (y1 > prevY2 + 1) {
488+
if (y1 > prevY2 + 1 || color !== prevColor) {
468489
// flush prev
469-
if (i !== 0) {
490+
if (i !== 0 && prevColor) {
470491
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
471492
}
472493
prevY1 = y1;
@@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart {
477498
prevY2 = y2;
478499
}
479500
}
501+
prevColor = color;
502+
canvasCtx.fillStyle = color;
503+
}
504+
if (prevColor) {
505+
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
480506
}
481-
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
482507
}
483508

484509
if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) {

src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class ViewCursorRenderData {
3535
) { }
3636
}
3737

38+
export enum CursorPlurality {
39+
Single,
40+
MultiPrimary,
41+
MultiSecondary,
42+
}
43+
3844
export class ViewCursor {
3945
private readonly _context: ViewContext;
4046
private readonly _domNode: FastDomNode<HTMLElement>;
@@ -47,11 +53,12 @@ export class ViewCursor {
4753
private _isVisible: boolean;
4854

4955
private _position: Position;
56+
private _pluralityClass: string;
5057

5158
private _lastRenderedContent: string;
5259
private _renderData: ViewCursorRenderData | null;
5360

54-
constructor(context: ViewContext) {
61+
constructor(context: ViewContext, plurality: CursorPlurality) {
5562
this._context = context;
5663
const options = this._context.configuration.options;
5764
const fontInfo = options.get(EditorOption.fontInfo);
@@ -73,6 +80,8 @@ export class ViewCursor {
7380
this._domNode.setDisplay('none');
7481

7582
this._position = new Position(1, 1);
83+
this._pluralityClass = '';
84+
this.setPlurality(plurality);
7685

7786
this._lastRenderedContent = '';
7887
this._renderData = null;
@@ -86,6 +95,23 @@ export class ViewCursor {
8695
return this._position;
8796
}
8897

98+
public setPlurality(plurality: CursorPlurality) {
99+
switch (plurality) {
100+
default:
101+
case CursorPlurality.Single:
102+
this._pluralityClass = '';
103+
break;
104+
105+
case CursorPlurality.MultiPrimary:
106+
this._pluralityClass = 'cursor-primary';
107+
break;
108+
109+
case CursorPlurality.MultiSecondary:
110+
this._pluralityClass = 'cursor-secondary';
111+
break;
112+
}
113+
}
114+
89115
public show(): void {
90116
if (!this._isVisible) {
91117
this._domNode.setVisibility('inherit');
@@ -229,7 +255,7 @@ export class ViewCursor {
229255
this._domNode.domNode.textContent = this._lastRenderedContent;
230256
}
231257

232-
this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
258+
this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
233259

234260
this._domNode.setDisplay('block');
235261
this._domNode.setTop(this._renderData.top);

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

+34-15
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import 'vs/css!./viewCursors';
77
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
88
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
99
import { ViewPart } from 'vs/editor/browser/view/viewPart';
10-
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
10+
import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
1111
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions';
1212
import { Position } from 'vs/editor/common/core/position';
13-
import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry';
13+
import {
14+
editorCursorBackground, editorCursorForeground,
15+
editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground,
16+
editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground
17+
} from 'vs/editor/common/core/editorColorRegistry';
1418
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
1519
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
1620
import * as viewEvents from 'vs/editor/common/viewEvents';
@@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart {
5761

5862
this._isVisible = false;
5963

60-
this._primaryCursor = new ViewCursor(this._context);
64+
this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
6165
this._secondaryCursors = [];
6266
this._renderData = [];
6367

@@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart {
8892
}
8993

9094
// --- begin event handlers
95+
9196
public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean {
9297
this._isComposingInput = true;
9398
this._updateBlinking();
@@ -120,14 +125,15 @@ export class ViewCursors extends ViewPart {
120125
this._secondaryCursors.length !== secondaryPositions.length
121126
|| (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit)
122127
);
128+
this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single);
123129
this._primaryCursor.onCursorPositionChanged(position, pauseAnimation);
124130
this._updateBlinking();
125131

126132
if (this._secondaryCursors.length < secondaryPositions.length) {
127133
// Create new cursors
128134
const addCnt = secondaryPositions.length - this._secondaryCursors.length;
129135
for (let i = 0; i < addCnt; i++) {
130-
const newCursor = new ViewCursor(this._context);
136+
const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary);
131137
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
132138
this._secondaryCursors.push(newCursor);
133139
}
@@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart {
160166

161167
return true;
162168
}
163-
164169
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
165170
// true for inline decorations that can end up relayouting text
166171
return true;
@@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart {
263268
}
264269
}
265270
}
271+
266272
// --- end blinking logic
267273

268274
private _updateDomClassName(): void {
@@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart {
375381
}
376382

377383
registerThemingParticipant((theme, collector) => {
378-
const caret = theme.getColor(editorCursorForeground);
379-
if (caret) {
380-
let caretBackground = theme.getColor(editorCursorBackground);
381-
if (!caretBackground) {
382-
caretBackground = caret.opposite();
383-
}
384-
collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
385-
if (isHighContrast(theme.type)) {
386-
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
384+
type CursorTheme = {
385+
foreground: string;
386+
background: string;
387+
class: string;
388+
};
389+
390+
const cursorThemes: CursorTheme[] = [
391+
{ class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground },
392+
{ class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground },
393+
{ class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground },
394+
];
395+
396+
for (const cursorTheme of cursorThemes) {
397+
const caret = theme.getColor(cursorTheme.foreground);
398+
if (caret) {
399+
let caretBackground = theme.getColor(cursorTheme.background);
400+
if (!caretBackground) {
401+
caretBackground = caret.opposite();
402+
}
403+
collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
404+
if (isHighContrast(theme.type)) {
405+
collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
406+
}
387407
}
388408
}
389-
390409
});

src/vs/editor/common/core/editorColorRegistry.ts

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight
2020

2121
export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.'));
2222
export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.'));
23+
export const editorMultiCursorPrimaryForeground = registerColor('editorMultiCursor.primary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorPrimaryForeground', 'Color of the primary editor cursor when multiple cursors are present.'));
24+
export const editorMultiCursorPrimaryBackground = registerColor('editorMultiCursor.primary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorPrimaryBackground', 'The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
25+
export const editorMultiCursorSecondaryForeground = registerColor('editorMultiCursor.secondary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorSecondaryForeground', 'Color of secondary editor cursors when multiple cursors are present.'));
26+
export const editorMultiCursorSecondaryBackground = registerColor('editorMultiCursor.secondary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorSecondaryBackground', 'The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
2327
export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.'));
2428
export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.'));
2529

0 commit comments

Comments
 (0)