Path: blob/main/src/vs/editor/browser/viewParts/whitespace/whitespace.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 './whitespace.css';6import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';7import { Selection } from '../../../common/core/selection.js';8import { RenderingContext } from '../../view/renderingContext.js';9import { ViewContext } from '../../../common/viewModel/viewContext.js';10import * as viewEvents from '../../../common/viewEvents.js';11import { ViewLineRenderingData } from '../../../common/viewModel.js';12import { EditorOption } from '../../../common/config/editorOptions.js';13import { IEditorConfiguration } from '../../../common/config/editorConfiguration.js';14import * as strings from '../../../../base/common/strings.js';15import { CharCode } from '../../../../base/common/charCode.js';16import { Position } from '../../../common/core/position.js';17import { editorWhitespaces } from '../../../common/core/editorColorRegistry.js';18import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';1920/**21* The whitespace overlay will visual certain whitespace depending on the22* current editor configuration (boundary, selection, etc.).23*/24export class WhitespaceOverlay extends DynamicViewOverlay {2526private readonly _context: ViewContext;27private _options: WhitespaceOptions;28private _selection: Selection[];29private _renderResult: string[] | null;3031constructor(context: ViewContext) {32super();33this._context = context;34this._options = new WhitespaceOptions(this._context.configuration);35this._selection = [];36this._renderResult = null;37this._context.addEventHandler(this);38}3940public override dispose(): void {41this._context.removeEventHandler(this);42this._renderResult = null;43super.dispose();44}4546// --- begin event handlers4748public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {49const newOptions = new WhitespaceOptions(this._context.configuration);50if (this._options.equals(newOptions)) {51return e.hasChanged(EditorOption.layoutInfo);52}53this._options = newOptions;54return true;55}56public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {57this._selection = e.selections;58if (this._options.renderWhitespace === 'selection') {59return true;60}61return false;62}63public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {64return true;65}66public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {67return true;68}69public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {70return true;71}72public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {73return true;74}75public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {76return true;77}78public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {79return e.scrollTopChanged;80}81public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {82return true;83}84// --- end event handlers8586public prepareRender(ctx: RenderingContext): void {87if (this._options.renderWhitespace === 'none') {88this._renderResult = null;89return;90}9192const startLineNumber = ctx.visibleRange.startLineNumber;93const endLineNumber = ctx.visibleRange.endLineNumber;94const lineCount = endLineNumber - startLineNumber + 1;95const needed = new Array<boolean>(lineCount);96for (let i = 0; i < lineCount; i++) {97needed[i] = true;98}99100this._renderResult = [];101for (let lineNumber = ctx.viewportData.startLineNumber; lineNumber <= ctx.viewportData.endLineNumber; lineNumber++) {102const lineIndex = lineNumber - ctx.viewportData.startLineNumber;103const lineData = this._context.viewModel.getViewLineRenderingData(lineNumber);104105let selectionsOnLine: OffsetRange[] | null = null;106if (this._options.renderWhitespace === 'selection') {107const selections = this._selection;108for (const selection of selections) {109110if (selection.endLineNumber < lineNumber || selection.startLineNumber > lineNumber) {111// Selection does not intersect line112continue;113}114115const startColumn = (selection.startLineNumber === lineNumber ? selection.startColumn : lineData.minColumn);116const endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn);117118if (startColumn < endColumn) {119if (!selectionsOnLine) {120selectionsOnLine = [];121}122selectionsOnLine.push(new OffsetRange(startColumn - 1, endColumn - 1));123}124}125}126127this._renderResult[lineIndex] = this._applyRenderWhitespace(ctx, lineNumber, selectionsOnLine, lineData);128}129}130131private _applyRenderWhitespace(ctx: RenderingContext, lineNumber: number, selections: OffsetRange[] | null, lineData: ViewLineRenderingData): string {132if (lineData.hasVariableFonts) {133return '';134}135if (this._options.renderWhitespace === 'selection' && !selections) {136return '';137}138if (this._options.renderWhitespace === 'trailing' && lineData.continuesWithWrappedLine) {139return '';140}141const color = this._context.theme.getColor(editorWhitespaces);142const USE_SVG = this._options.renderWithSVG;143144const lineContent = lineData.content;145const len = (this._options.stopRenderingLineAfter === -1 ? lineContent.length : Math.min(this._options.stopRenderingLineAfter, lineContent.length));146const continuesWithWrappedLine = lineData.continuesWithWrappedLine;147const fauxIndentLength = lineData.minColumn - 1;148const onlyBoundary = (this._options.renderWhitespace === 'boundary');149const onlyTrailing = (this._options.renderWhitespace === 'trailing');150const lineHeight = ctx.getLineHeightForLineNumber(lineNumber);151const middotWidth = this._options.middotWidth;152const wsmiddotWidth = this._options.wsmiddotWidth;153const spaceWidth = this._options.spaceWidth;154const wsmiddotDiff = Math.abs(wsmiddotWidth - spaceWidth);155const middotDiff = Math.abs(middotWidth - spaceWidth);156157// U+2E31 - WORD SEPARATOR MIDDLE DOT158// U+00B7 - MIDDLE DOT159const renderSpaceCharCode = (wsmiddotDiff < middotDiff ? 0x2E31 : 0xB7);160161const canUseHalfwidthRightwardsArrow = this._options.canUseHalfwidthRightwardsArrow;162163let result: string = '';164165let lineIsEmptyOrWhitespace = false;166let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);167let lastNonWhitespaceIndex: number;168if (firstNonWhitespaceIndex === -1) {169lineIsEmptyOrWhitespace = true;170firstNonWhitespaceIndex = len;171lastNonWhitespaceIndex = len;172} else {173lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);174}175176let currentSelectionIndex = 0;177let currentSelection = selections && selections[currentSelectionIndex];178let maxLeft = 0;179180for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {181const chCode = lineContent.charCodeAt(charIndex);182183if (currentSelection && currentSelection.endExclusive <= charIndex) {184currentSelectionIndex++;185currentSelection = selections && selections[currentSelectionIndex];186}187188if (chCode !== CharCode.Tab && chCode !== CharCode.Space) {189continue;190}191192if (onlyTrailing && !lineIsEmptyOrWhitespace && charIndex <= lastNonWhitespaceIndex) {193// If rendering only trailing whitespace, check that the charIndex points to trailing whitespace.194continue;195}196197if (onlyBoundary && charIndex >= firstNonWhitespaceIndex && charIndex <= lastNonWhitespaceIndex && chCode === CharCode.Space) {198// rendering only boundary whitespace199const prevChCode = (charIndex - 1 >= 0 ? lineContent.charCodeAt(charIndex - 1) : CharCode.Null);200const nextChCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);201if (prevChCode !== CharCode.Space && nextChCode !== CharCode.Space) {202continue;203}204}205206if (onlyBoundary && continuesWithWrappedLine && charIndex === len - 1) {207const prevCharCode = (charIndex - 1 >= 0 ? lineContent.charCodeAt(charIndex - 1) : CharCode.Null);208const isSingleTrailingSpace = (chCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab));209if (isSingleTrailingSpace) {210continue;211}212}213214if (selections && !(currentSelection && currentSelection.start <= charIndex && charIndex < currentSelection.endExclusive)) {215// If rendering whitespace on selection, check that the charIndex falls within a selection216continue;217}218219const visibleRange = ctx.visibleRangeForPosition(new Position(lineNumber, charIndex + 1));220if (!visibleRange) {221continue;222}223224if (USE_SVG) {225maxLeft = Math.max(maxLeft, visibleRange.left);226if (chCode === CharCode.Tab) {227result += this._renderArrow(lineHeight, spaceWidth, visibleRange.left);228} else {229result += `<circle cx="${(visibleRange.left + spaceWidth / 2).toFixed(2)}" cy="${(lineHeight / 2).toFixed(2)}" r="${(spaceWidth / 7).toFixed(2)}" />`;230}231} else {232if (chCode === CharCode.Tab) {233result += `<div class="mwh" style="left:${visibleRange.left}px;height:${lineHeight}px;">${canUseHalfwidthRightwardsArrow ? String.fromCharCode(0xFFEB) : String.fromCharCode(0x2192)}</div>`;234} else {235result += `<div class="mwh" style="left:${visibleRange.left}px;height:${lineHeight}px;">${String.fromCharCode(renderSpaceCharCode)}</div>`;236}237}238}239240if (USE_SVG) {241maxLeft = Math.round(maxLeft + spaceWidth);242return (243`<svg style="bottom:0;position:absolute;width:${maxLeft}px;height:${lineHeight}px" viewBox="0 0 ${maxLeft} ${lineHeight}" xmlns="http://www.w3.org/2000/svg" fill="${color}">`244+ result245+ `</svg>`246);247}248249return result;250}251252private _renderArrow(lineHeight: number, spaceWidth: number, left: number): string {253const strokeWidth = spaceWidth / 7;254const width = spaceWidth;255const dy = lineHeight / 2;256const dx = left;257258const p1 = { x: 0, y: strokeWidth / 2 };259const p2 = { x: 100 / 125 * width, y: p1.y };260const p3 = { x: p2.x - 0.2 * p2.x, y: p2.y + 0.2 * p2.x };261const p4 = { x: p3.x + 0.1 * p2.x, y: p3.y + 0.1 * p2.x };262const p5 = { x: p4.x + 0.35 * p2.x, y: p4.y - 0.35 * p2.x };263const p6 = { x: p5.x, y: -p5.y };264const p7 = { x: p4.x, y: -p4.y };265const p8 = { x: p3.x, y: -p3.y };266const p9 = { x: p2.x, y: -p2.y };267const p10 = { x: p1.x, y: -p1.y };268269const p = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10];270const parts = p.map((p) => `${(dx + p.x).toFixed(2)} ${(dy + p.y).toFixed(2)}`).join(' L ');271return `<path d="M ${parts}" />`;272}273274public render(startLineNumber: number, lineNumber: number): string {275if (!this._renderResult) {276return '';277}278const lineIndex = lineNumber - startLineNumber;279if (lineIndex < 0 || lineIndex >= this._renderResult.length) {280return '';281}282return this._renderResult[lineIndex];283}284}285286class WhitespaceOptions {287288public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';289public readonly renderWithSVG: boolean;290public readonly spaceWidth: number;291public readonly middotWidth: number;292public readonly wsmiddotWidth: number;293public readonly canUseHalfwidthRightwardsArrow: boolean;294public readonly lineHeight: number;295public readonly stopRenderingLineAfter: number;296297constructor(config: IEditorConfiguration) {298const options = config.options;299const fontInfo = options.get(EditorOption.fontInfo);300const experimentalWhitespaceRendering = options.get(EditorOption.experimentalWhitespaceRendering);301if (experimentalWhitespaceRendering === 'off') {302// whitespace is rendered in the view line303this.renderWhitespace = 'none';304this.renderWithSVG = false;305} else if (experimentalWhitespaceRendering === 'svg') {306this.renderWhitespace = options.get(EditorOption.renderWhitespace);307this.renderWithSVG = true;308} else {309this.renderWhitespace = options.get(EditorOption.renderWhitespace);310this.renderWithSVG = false;311}312this.spaceWidth = fontInfo.spaceWidth;313this.middotWidth = fontInfo.middotWidth;314this.wsmiddotWidth = fontInfo.wsmiddotWidth;315this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow;316this.lineHeight = options.get(EditorOption.lineHeight);317this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter);318}319320public equals(other: WhitespaceOptions): boolean {321return (322this.renderWhitespace === other.renderWhitespace323&& this.renderWithSVG === other.renderWithSVG324&& this.spaceWidth === other.spaceWidth325&& this.middotWidth === other.middotWidth326&& this.wsmiddotWidth === other.wsmiddotWidth327&& this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow328&& this.lineHeight === other.lineHeight329&& this.stopRenderingLineAfter === other.stopRenderingLineAfter330);331}332}333334335