Path: blob/main/src/vs/editor/browser/viewParts/selections/selections.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './selections.css';6import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';7import { Range } from '../../../common/core/range.js';8import { HorizontalRange, LineVisibleRanges, RenderingContext } from '../../view/renderingContext.js';9import { ViewContext } from '../../../common/viewModel/viewContext.js';10import * as viewEvents from '../../../common/viewEvents.js';11import { editorSelectionForeground } from '../../../../platform/theme/common/colorRegistry.js';12import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';13import { EditorOption } from '../../../common/config/editorOptions.js';1415const enum CornerStyle {16EXTERN,17INTERN,18FLAT19}2021interface IVisibleRangeEndPointStyle {22top: CornerStyle;23bottom: CornerStyle;24}2526class HorizontalRangeWithStyle {27public left: number;28public width: number;29public startStyle: IVisibleRangeEndPointStyle | null;30public endStyle: IVisibleRangeEndPointStyle | null;3132constructor(other: HorizontalRange) {33this.left = other.left;34this.width = other.width;35this.startStyle = null;36this.endStyle = null;37}38}3940class LineVisibleRangesWithStyle {41public lineNumber: number;42public ranges: HorizontalRangeWithStyle[];4344constructor(lineNumber: number, ranges: HorizontalRangeWithStyle[]) {45this.lineNumber = lineNumber;46this.ranges = ranges;47}48}4950function toStyledRange(item: HorizontalRange): HorizontalRangeWithStyle {51return new HorizontalRangeWithStyle(item);52}5354function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle {55return new LineVisibleRangesWithStyle(item.lineNumber, item.ranges.map(toStyledRange));56}5758/**59* This view part displays selected text to the user. Every line has its own selection overlay.60*/61export class SelectionsOverlay extends DynamicViewOverlay {6263private static readonly SELECTION_CLASS_NAME = 'selected-text';64private static readonly SELECTION_TOP_LEFT = 'top-left-radius';65private static readonly SELECTION_BOTTOM_LEFT = 'bottom-left-radius';66private static readonly SELECTION_TOP_RIGHT = 'top-right-radius';67private static readonly SELECTION_BOTTOM_RIGHT = 'bottom-right-radius';68private static readonly EDITOR_BACKGROUND_CLASS_NAME = 'monaco-editor-background';6970private static readonly ROUNDED_PIECE_WIDTH = 10;7172private readonly _context: ViewContext;73private _roundedSelection: boolean;74private _typicalHalfwidthCharacterWidth: number;75private _selections: Range[];76private _renderResult: string[] | null;7778constructor(context: ViewContext) {79super();80this._context = context;81const options = this._context.configuration.options;82this._roundedSelection = options.get(EditorOption.roundedSelection);83this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;84this._selections = [];85this._renderResult = null;86this._context.addEventHandler(this);87}8889public override dispose(): void {90this._context.removeEventHandler(this);91this._renderResult = null;92super.dispose();93}9495// --- begin event handlers9697public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {98const options = this._context.configuration.options;99this._roundedSelection = options.get(EditorOption.roundedSelection);100this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;101return true;102}103public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {104this._selections = e.selections.slice(0);105return true;106}107public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {108// true for inline decorations that can end up relayouting text109return true;//e.inlineDecorationsChanged;110}111public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {112return true;113}114public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {115return true;116}117public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {118return true;119}120public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {121return true;122}123public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {124return e.scrollTopChanged;125}126public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {127return true;128}129130// --- end event handlers131132private _visibleRangesHaveGaps(linesVisibleRanges: LineVisibleRangesWithStyle[]): boolean {133134for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {135const lineVisibleRanges = linesVisibleRanges[i];136137if (lineVisibleRanges.ranges.length > 1) {138// There are two ranges on the same line139return true;140}141}142143return false;144}145146private _enrichVisibleRangesWithStyle(viewport: Range, linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[] | null): void {147const epsilon = this._typicalHalfwidthCharacterWidth / 4;148let previousFrameTop: HorizontalRangeWithStyle | null = null;149let previousFrameBottom: HorizontalRangeWithStyle | null = null;150151if (previousFrame && previousFrame.length > 0 && linesVisibleRanges.length > 0) {152153const topLineNumber = linesVisibleRanges[0].lineNumber;154if (topLineNumber === viewport.startLineNumber) {155for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) {156if (previousFrame[i].lineNumber === topLineNumber) {157previousFrameTop = previousFrame[i].ranges[0];158}159}160}161162const bottomLineNumber = linesVisibleRanges[linesVisibleRanges.length - 1].lineNumber;163if (bottomLineNumber === viewport.endLineNumber) {164for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) {165if (previousFrame[i].lineNumber === bottomLineNumber) {166previousFrameBottom = previousFrame[i].ranges[0];167}168}169}170171if (previousFrameTop && !previousFrameTop.startStyle) {172previousFrameTop = null;173}174if (previousFrameBottom && !previousFrameBottom.startStyle) {175previousFrameBottom = null;176}177}178179for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {180// We know for a fact that there is precisely one range on each line181const curLineRange = linesVisibleRanges[i].ranges[0];182const curLeft = curLineRange.left;183const curRight = curLineRange.left + curLineRange.width;184185const startStyle = {186top: CornerStyle.EXTERN,187bottom: CornerStyle.EXTERN188};189190const endStyle = {191top: CornerStyle.EXTERN,192bottom: CornerStyle.EXTERN193};194195if (i > 0) {196// Look above197const prevLeft = linesVisibleRanges[i - 1].ranges[0].left;198const prevRight = linesVisibleRanges[i - 1].ranges[0].left + linesVisibleRanges[i - 1].ranges[0].width;199200if (abs(curLeft - prevLeft) < epsilon) {201startStyle.top = CornerStyle.FLAT;202} else if (curLeft > prevLeft) {203startStyle.top = CornerStyle.INTERN;204}205206if (abs(curRight - prevRight) < epsilon) {207endStyle.top = CornerStyle.FLAT;208} else if (prevLeft < curRight && curRight < prevRight) {209endStyle.top = CornerStyle.INTERN;210}211} else if (previousFrameTop) {212// Accept some hiccups near the viewport edges to save on repaints213startStyle.top = previousFrameTop.startStyle!.top;214endStyle.top = previousFrameTop.endStyle!.top;215}216217if (i + 1 < len) {218// Look below219const nextLeft = linesVisibleRanges[i + 1].ranges[0].left;220const nextRight = linesVisibleRanges[i + 1].ranges[0].left + linesVisibleRanges[i + 1].ranges[0].width;221222if (abs(curLeft - nextLeft) < epsilon) {223startStyle.bottom = CornerStyle.FLAT;224} else if (nextLeft < curLeft && curLeft < nextRight) {225startStyle.bottom = CornerStyle.INTERN;226}227228if (abs(curRight - nextRight) < epsilon) {229endStyle.bottom = CornerStyle.FLAT;230} else if (curRight < nextRight) {231endStyle.bottom = CornerStyle.INTERN;232}233} else if (previousFrameBottom) {234// Accept some hiccups near the viewport edges to save on repaints235startStyle.bottom = previousFrameBottom.startStyle!.bottom;236endStyle.bottom = previousFrameBottom.endStyle!.bottom;237}238239curLineRange.startStyle = startStyle;240curLineRange.endStyle = endStyle;241}242}243244private _getVisibleRangesWithStyle(selection: Range, ctx: RenderingContext, previousFrame: LineVisibleRangesWithStyle[] | null): LineVisibleRangesWithStyle[] {245const _linesVisibleRanges = ctx.linesVisibleRangesForRange(selection, true) || [];246const linesVisibleRanges = _linesVisibleRanges.map(toStyled);247const visibleRangesHaveGaps = this._visibleRangesHaveGaps(linesVisibleRanges);248249if (!visibleRangesHaveGaps && this._roundedSelection) {250this._enrichVisibleRangesWithStyle(ctx.visibleRange, linesVisibleRanges, previousFrame);251}252253// The visible ranges are sorted TOP-BOTTOM and LEFT-RIGHT254return linesVisibleRanges;255}256257private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string {258return (259'<div class="cslr '260+ className261+ '" style="'262+ 'top:' + top.toString() + 'px;'263+ 'bottom:' + bottom.toString() + 'px;'264+ 'left:' + left.toString() + 'px;'265+ 'width:' + width.toString() + 'px;'266+ '"></div>'267);268}269270private _actualRenderOneSelection(output2: [string, string][], visibleStartLineNumber: number, hasMultipleSelections: boolean, visibleRanges: LineVisibleRangesWithStyle[]): void {271if (visibleRanges.length === 0) {272return;273}274275const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle;276277const firstLineNumber = visibleRanges[0].lineNumber;278const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber;279280for (let i = 0, len = visibleRanges.length; i < len; i++) {281const lineVisibleRanges = visibleRanges[i];282const lineNumber = lineVisibleRanges.lineNumber;283const lineIndex = lineNumber - visibleStartLineNumber;284285const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0;286const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0;287288let innerCornerOutput = '';289let restOfSelectionOutput = '';290291for (let j = 0, lenJ = lineVisibleRanges.ranges.length; j < lenJ; j++) {292const visibleRange = lineVisibleRanges.ranges[j];293294if (visibleRangesHaveStyle) {295const startStyle = visibleRange.startStyle!;296const endStyle = visibleRange.endStyle!;297if (startStyle.top === CornerStyle.INTERN || startStyle.bottom === CornerStyle.INTERN) {298// Reverse rounded corner to the left299300// First comes the selection (blue layer)301innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);302303// Second comes the background (white layer) with inverse border radius304let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;305if (startStyle.top === CornerStyle.INTERN) {306className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;307}308if (startStyle.bottom === CornerStyle.INTERN) {309className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;310}311innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);312}313if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) {314// Reverse rounded corner to the right315316// First comes the selection (blue layer)317innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);318319// Second comes the background (white layer) with inverse border radius320let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;321if (endStyle.top === CornerStyle.INTERN) {322className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;323}324if (endStyle.bottom === CornerStyle.INTERN) {325className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;326}327innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);328}329}330331let className = SelectionsOverlay.SELECTION_CLASS_NAME;332if (visibleRangesHaveStyle) {333const startStyle = visibleRange.startStyle!;334const endStyle = visibleRange.endStyle!;335if (startStyle.top === CornerStyle.EXTERN) {336className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;337}338if (startStyle.bottom === CornerStyle.EXTERN) {339className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;340}341if (endStyle.top === CornerStyle.EXTERN) {342className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;343}344if (endStyle.bottom === CornerStyle.EXTERN) {345className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;346}347}348restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width);349}350351output2[lineIndex][0] += innerCornerOutput;352output2[lineIndex][1] += restOfSelectionOutput;353}354}355356private _previousFrameVisibleRangesWithStyle: (LineVisibleRangesWithStyle[] | null)[] = [];357public prepareRender(ctx: RenderingContext): void {358359// Build HTML for inner corners separate from HTML for the rest of selections,360// as the inner corner HTML can interfere with that of other selections.361// In final render, make sure to place the inner corner HTML before the rest of selection HTML. See issue #77777.362const output: [string, string][] = [];363const visibleStartLineNumber = ctx.visibleRange.startLineNumber;364const visibleEndLineNumber = ctx.visibleRange.endLineNumber;365for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {366const lineIndex = lineNumber - visibleStartLineNumber;367output[lineIndex] = ['', ''];368}369370const thisFrameVisibleRangesWithStyle: (LineVisibleRangesWithStyle[] | null)[] = [];371for (let i = 0, len = this._selections.length; i < len; i++) {372const selection = this._selections[i];373if (selection.isEmpty()) {374thisFrameVisibleRangesWithStyle[i] = null;375continue;376}377378const visibleRangesWithStyle = this._getVisibleRangesWithStyle(selection, ctx, this._previousFrameVisibleRangesWithStyle[i]);379thisFrameVisibleRangesWithStyle[i] = visibleRangesWithStyle;380this._actualRenderOneSelection(output, visibleStartLineNumber, this._selections.length > 1, visibleRangesWithStyle);381}382383this._previousFrameVisibleRangesWithStyle = thisFrameVisibleRangesWithStyle;384this._renderResult = output.map(([internalCorners, restOfSelection]) => internalCorners + restOfSelection);385}386387public render(startLineNumber: number, lineNumber: number): string {388if (!this._renderResult) {389return '';390}391const lineIndex = lineNumber - startLineNumber;392if (lineIndex < 0 || lineIndex >= this._renderResult.length) {393return '';394}395return this._renderResult[lineIndex];396}397}398399registerThemingParticipant((theme, collector) => {400const editorSelectionForegroundColor = theme.getColor(editorSelectionForeground);401if (editorSelectionForegroundColor && !editorSelectionForegroundColor.isTransparent()) {402collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`);403}404});405406function abs(n: number): number {407return n < 0 ? -n : n;408}409410411