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>
This commit is contained in:
Adam Byrd 2024-03-20 11:24:59 -07:00 committed by GitHub
parent 763ad71a17
commit c37edea794
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 108 additions and 34 deletions

View file

@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { Position } from 'vs/editor/common/core/position';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
import { TokenizationRegistry } from 'vs/editor/common/languages';
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry';
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme';
@ -29,7 +29,9 @@ class Settings {
public readonly borderColor: string | null;
public readonly hideCursor: boolean;
public readonly cursorColor: string | null;
public readonly cursorColorSingle: string | null;
public readonly cursorColorPrimary: string | null;
public readonly cursorColorSecondary: string | null;
public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark';
public readonly backgroundColor: Color | null;
@ -55,8 +57,12 @@ class Settings {
this.borderColor = borderColor ? borderColor.toString() : null;
this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler);
const cursorColor = theme.getColor(editorCursorForeground);
this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
const cursorColorSingle = theme.getColor(editorCursorForeground);
this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null;
const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground);
this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null;
const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground);
this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null;
this.themeType = theme.type;
@ -189,7 +195,9 @@ class Settings {
&& this.renderBorder === other.renderBorder
&& this.borderColor === other.borderColor
&& this.hideCursor === other.hideCursor
&& this.cursorColor === other.cursorColor
&& this.cursorColorSingle === other.cursorColorSingle
&& this.cursorColorPrimary === other.cursorColorPrimary
&& this.cursorColorSecondary === other.cursorColorSecondary
&& this.themeType === other.themeType
&& Color.equals(this.backgroundColor, other.backgroundColor)
&& this.top === other.top
@ -213,6 +221,11 @@ const enum OverviewRulerLane {
Full = 7
}
type Cursor = {
position: Position;
color: string | null;
};
const enum ShouldRenderValue {
NotNeeded = 0,
Maybe = 1,
@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart {
private readonly _tokensColorTrackerListener: IDisposable;
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private _settings!: Settings;
private _cursorPositions: Position[];
private _cursorPositions: Cursor[];
private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
private _renderedCursorPositions: Position[] = [];
private _renderedCursorPositions: Cursor[] = [];
constructor(context: ViewContext) {
super(context);
@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart {
}
});
this._cursorPositions = [new Position(1, 1)];
this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }];
}
public override dispose(): void {
@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart {
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._cursorPositions = [];
for (let i = 0, len = e.selections.length; i < len; i++) {
this._cursorPositions[i] = e.selections[i].getPosition();
let color = this._settings.cursorColorSingle;
if (len > 1) {
color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary;
}
this._cursorPositions.push({ position: e.selections[i].getPosition(), color });
}
this._cursorPositions.sort(Position.compare);
this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position));
return this._markRenderingIsMaybeNeeded();
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart {
if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) {
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe) {
@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart {
}
// Draw cursors
if (!this._settings.hideCursor && this._settings.cursorColor) {
if (!this._settings.hideCursor) {
const cursorHeight = (2 * this._settings.pixelRatio) | 0;
const halfCursorHeight = (cursorHeight / 2) | 0;
const cursorX = this._settings.x[OverviewRulerLane.Full];
const cursorW = this._settings.w[OverviewRulerLane.Full];
canvasCtx.fillStyle = this._settings.cursorColor;
let prevY1 = -100;
let prevY2 = -100;
let prevColor: string | null = null;
for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
const cursor = this._cursorPositions[i];
const color = this._cursorPositions[i].color;
if (!color) {
continue;
}
const cursor = this._cursorPositions[i].position;
let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0;
if (yCenter < halfCursorHeight) {
@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart {
const y1 = yCenter - halfCursorHeight;
const y2 = y1 + cursorHeight;
if (y1 > prevY2 + 1) {
if (y1 > prevY2 + 1 || color !== prevColor) {
// flush prev
if (i !== 0) {
if (i !== 0 && prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
prevY1 = y1;
@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart {
prevY2 = y2;
}
}
prevColor = color;
canvasCtx.fillStyle = color;
}
if (prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) {

View file

@ -35,6 +35,12 @@ class ViewCursorRenderData {
) { }
}
export enum CursorPlurality {
Single,
MultiPrimary,
MultiSecondary,
}
export class ViewCursor {
private readonly _context: ViewContext;
private readonly _domNode: FastDomNode<HTMLElement>;
@ -47,11 +53,12 @@ export class ViewCursor {
private _isVisible: boolean;
private _position: Position;
private _pluralityClass: string;
private _lastRenderedContent: string;
private _renderData: ViewCursorRenderData | null;
constructor(context: ViewContext) {
constructor(context: ViewContext, plurality: CursorPlurality) {
this._context = context;
const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo);
@ -73,6 +80,8 @@ export class ViewCursor {
this._domNode.setDisplay('none');
this._position = new Position(1, 1);
this._pluralityClass = '';
this.setPlurality(plurality);
this._lastRenderedContent = '';
this._renderData = null;
@ -86,6 +95,23 @@ export class ViewCursor {
return this._position;
}
public setPlurality(plurality: CursorPlurality) {
switch (plurality) {
default:
case CursorPlurality.Single:
this._pluralityClass = '';
break;
case CursorPlurality.MultiPrimary:
this._pluralityClass = 'cursor-primary';
break;
case CursorPlurality.MultiSecondary:
this._pluralityClass = 'cursor-secondary';
break;
}
}
public show(): void {
if (!this._isVisible) {
this._domNode.setVisibility('inherit');
@ -229,7 +255,7 @@ export class ViewCursor {
this._domNode.domNode.textContent = this._lastRenderedContent;
}
this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
this._domNode.setDisplay('block');
this._domNode.setTop(this._renderData.top);

View file

@ -7,10 +7,14 @@ import 'vs/css!./viewCursors';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry';
import {
editorCursorBackground, editorCursorForeground,
editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground,
editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground
} from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import * as viewEvents from 'vs/editor/common/viewEvents';
@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart {
this._isVisible = false;
this._primaryCursor = new ViewCursor(this._context);
this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
this._secondaryCursors = [];
this._renderData = [];
@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart {
}
// --- begin event handlers
public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean {
this._isComposingInput = true;
this._updateBlinking();
@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart {
this._secondaryCursors.length !== secondaryPositions.length
|| (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit)
);
this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single);
this._primaryCursor.onCursorPositionChanged(position, pauseAnimation);
this._updateBlinking();
@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart {
// Create new cursors
const addCnt = secondaryPositions.length - this._secondaryCursors.length;
for (let i = 0; i < addCnt; i++) {
const newCursor = new ViewCursor(this._context);
const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary);
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
this._secondaryCursors.push(newCursor);
}
@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart {
return true;
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart {
}
}
}
// --- end blinking logic
private _updateDomClassName(): void {
@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart {
}
registerThemingParticipant((theme, collector) => {
const caret = theme.getColor(editorCursorForeground);
if (caret) {
let caretBackground = theme.getColor(editorCursorBackground);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
type CursorTheme = {
foreground: string;
background: string;
class: string;
};
const cursorThemes: CursorTheme[] = [
{ class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground },
{ class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground },
{ class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground },
];
for (const cursorTheme of cursorThemes) {
const caret = theme.getColor(cursorTheme.foreground);
if (caret) {
let caretBackground = theme.getColor(cursorTheme.background);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
}
}
}
});

View file

@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight
export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.'));
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.'));
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.'));
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.'));
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.'));
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.'));
export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.'));
export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.'));