UNPKG

monaco-editor

Version:
544 lines (543 loc) • 26.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as browser from '../../../../base/browser/browser.js'; import { createFastDomNode } from '../../../../base/browser/fastDomNode.js'; import * as platform from '../../../../base/common/platform.js'; import { RangeUtil } from './rangeUtil.js'; import { FloatHorizontalRange, VisibleRanges } from '../../view/renderingContext.js'; import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { RenderLineInput, renderViewLine, LineRange, DomPosition } from '../../../common/viewLayout/viewLineRenderer.js'; import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { // In VSCode we know very well when the zoom level changes return true; } if (platform.isLinux || browser.isFirefox || browser.isSafari) { // On Linux, it appears that zooming affects char widths (in pixels), which is unexpected. // -- // Even though we read character widths correctly, having read them at a specific zoom level // does not mean they are the same at the current zoom level. // -- // This could be improved if we ever figure out how to get an event when browsers zoom, // but until then we have to stick with reading client rects. // -- // The same has been observed with Firefox on Windows7 // -- // The same has been oversved with Safari return false; } return true; })(); let monospaceAssumptionsAreValid = true; export class ViewLineOptions { constructor(config, themeType) { this.themeType = themeType; const options = config.options; const fontInfo = options.get(50 /* EditorOption.fontInfo */); const experimentalWhitespaceRendering = options.get(38 /* EditorOption.experimentalWhitespaceRendering */); if (experimentalWhitespaceRendering === 'off') { this.renderWhitespace = options.get(100 /* EditorOption.renderWhitespace */); } else { // whitespace is rendered in a different layer this.renderWhitespace = 'none'; } this.renderControlCharacters = options.get(95 /* EditorOption.renderControlCharacters */); this.spaceWidth = fontInfo.spaceWidth; this.middotWidth = fontInfo.middotWidth; this.wsmiddotWidth = fontInfo.wsmiddotWidth; this.useMonospaceOptimizations = (fontInfo.isMonospace && !options.get(33 /* EditorOption.disableMonospaceOptimizations */)); this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow; this.lineHeight = options.get(67 /* EditorOption.lineHeight */); this.stopRenderingLineAfter = options.get(118 /* EditorOption.stopRenderingLineAfter */); this.fontLigatures = options.get(51 /* EditorOption.fontLigatures */); } equals(other) { return (this.themeType === other.themeType && this.renderWhitespace === other.renderWhitespace && this.renderControlCharacters === other.renderControlCharacters && this.spaceWidth === other.spaceWidth && this.middotWidth === other.middotWidth && this.wsmiddotWidth === other.wsmiddotWidth && this.useMonospaceOptimizations === other.useMonospaceOptimizations && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow && this.lineHeight === other.lineHeight && this.stopRenderingLineAfter === other.stopRenderingLineAfter && this.fontLigatures === other.fontLigatures); } } export class ViewLine { static { this.CLASS_NAME = 'view-line'; } constructor(options) { this._options = options; this._isMaybeInvalid = true; this._renderedViewLine = null; } // --- begin IVisibleLineData getDomNode() { if (this._renderedViewLine && this._renderedViewLine.domNode) { return this._renderedViewLine.domNode.domNode; } return null; } setDomNode(domNode) { if (this._renderedViewLine) { this._renderedViewLine.domNode = createFastDomNode(domNode); } else { throw new Error('I have no rendered view line to set the dom node to...'); } } onContentChanged() { this._isMaybeInvalid = true; } onTokensChanged() { this._isMaybeInvalid = true; } onDecorationsChanged() { this._isMaybeInvalid = true; } onOptionsChanged(newOptions) { this._isMaybeInvalid = true; this._options = newOptions; } onSelectionChanged() { if (isHighContrast(this._options.themeType) || this._options.renderWhitespace === 'selection') { this._isMaybeInvalid = true; return true; } return false; } renderLine(lineNumber, deltaTop, lineHeight, viewportData, sb) { if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; } this._isMaybeInvalid = false; const lineData = viewportData.getViewLineRenderingData(lineNumber); const options = this._options; const actualInlineDecorations = LineDecoration.filter(lineData.inlineDecorations, lineNumber, lineData.minColumn, lineData.maxColumn); // Only send selection information when needed for rendering whitespace let selectionsOnLine = null; if (isHighContrast(options.themeType) || this._options.renderWhitespace === 'selection') { const selections = viewportData.selections; for (const selection of selections) { if (selection.endLineNumber < lineNumber || selection.startLineNumber > lineNumber) { // Selection does not intersect line continue; } const startColumn = (selection.startLineNumber === lineNumber ? selection.startColumn : lineData.minColumn); const endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn); if (startColumn < endColumn) { if (isHighContrast(options.themeType)) { actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', 0 /* InlineDecorationType.Regular */)); } if (this._options.renderWhitespace === 'selection') { if (!selectionsOnLine) { selectionsOnLine = []; } selectionsOnLine.push(new LineRange(startColumn - 1, endColumn - 1)); } } } } const renderLineInput = new RenderLineInput(options.useMonospaceOptimizations, options.canUseHalfwidthRightwardsArrow, lineData.content, lineData.continuesWithWrappedLine, lineData.isBasicASCII, lineData.containsRTL, lineData.minColumn - 1, lineData.tokens, actualInlineDecorations, lineData.tabSize, lineData.startVisibleColumn, options.spaceWidth, options.middotWidth, options.wsmiddotWidth, options.stopRenderingLineAfter, options.renderWhitespace, options.renderControlCharacters, options.fontLigatures !== EditorFontLigatures.OFF, selectionsOnLine); if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) { // no need to do anything, we have the same render input return false; } sb.appendString('<div style="top:'); sb.appendString(String(deltaTop)); sb.appendString('px;height:'); sb.appendString(String(lineHeight)); sb.appendString('px;" class="'); sb.appendString(ViewLine.CLASS_NAME); sb.appendString('">'); const output = renderViewLine(renderLineInput, sb); sb.appendString('</div>'); let renderedViewLine = null; if (monospaceAssumptionsAreValid && canUseFastRenderedViewLine && lineData.isBasicASCII && options.useMonospaceOptimizations && output.containsForeignElements === 0 /* ForeignElementType.None */) { renderedViewLine = new FastRenderedViewLine(this._renderedViewLine ? this._renderedViewLine.domNode : null, renderLineInput, output.characterMapping); } if (!renderedViewLine) { renderedViewLine = createRenderedLine(this._renderedViewLine ? this._renderedViewLine.domNode : null, renderLineInput, output.characterMapping, output.containsRTL, output.containsForeignElements); } this._renderedViewLine = renderedViewLine; return true; } layoutLine(lineNumber, deltaTop, lineHeight) { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); this._renderedViewLine.domNode.setHeight(lineHeight); } } // --- end IVisibleLineData getWidth(context) { if (!this._renderedViewLine) { return 0; } return this._renderedViewLine.getWidth(context); } getWidthIsFast() { if (!this._renderedViewLine) { return true; } return this._renderedViewLine.getWidthIsFast(); } needsMonospaceFontCheck() { if (!this._renderedViewLine) { return false; } return (this._renderedViewLine instanceof FastRenderedViewLine); } monospaceAssumptionsAreValid() { if (!this._renderedViewLine) { return monospaceAssumptionsAreValid; } if (this._renderedViewLine instanceof FastRenderedViewLine) { return this._renderedViewLine.monospaceAssumptionsAreValid(); } return monospaceAssumptionsAreValid; } onMonospaceAssumptionsInvalidated() { if (this._renderedViewLine && this._renderedViewLine instanceof FastRenderedViewLine) { this._renderedViewLine = this._renderedViewLine.toSlowRenderedLine(); } } getVisibleRangesForRange(lineNumber, startColumn, endColumn, context) { if (!this._renderedViewLine) { return null; } startColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, startColumn)); endColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, endColumn)); const stopRenderingLineAfter = this._renderedViewLine.input.stopRenderingLineAfter; if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter + 1 && endColumn > stopRenderingLineAfter + 1) { // This range is obviously not visible return new VisibleRanges(true, [new FloatHorizontalRange(this.getWidth(context), 0)]); } if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter + 1) { startColumn = stopRenderingLineAfter + 1; } if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter + 1) { endColumn = stopRenderingLineAfter + 1; } const horizontalRanges = this._renderedViewLine.getVisibleRangesForRange(lineNumber, startColumn, endColumn, context); if (horizontalRanges && horizontalRanges.length > 0) { return new VisibleRanges(false, horizontalRanges); } return null; } getColumnOfNodeOffset(spanNode, offset) { if (!this._renderedViewLine) { return 1; } return this._renderedViewLine.getColumnOfNodeOffset(spanNode, offset); } } /** * A rendered line which is guaranteed to contain only regular ASCII and is rendered with a monospace font. */ class FastRenderedViewLine { constructor(domNode, renderLineInput, characterMapping) { this._cachedWidth = -1; this.domNode = domNode; this.input = renderLineInput; const keyColumnCount = Math.floor(renderLineInput.lineContent.length / 300 /* Constants.MaxMonospaceDistance */); if (keyColumnCount > 0) { this._keyColumnPixelOffsetCache = new Float32Array(keyColumnCount); for (let i = 0; i < keyColumnCount; i++) { this._keyColumnPixelOffsetCache[i] = -1; } } else { this._keyColumnPixelOffsetCache = null; } this._characterMapping = characterMapping; this._charWidth = renderLineInput.spaceWidth; } getWidth(context) { if (!this.domNode || this.input.lineContent.length < 300 /* Constants.MaxMonospaceDistance */) { const horizontalOffset = this._characterMapping.getHorizontalOffset(this._characterMapping.length); return Math.round(this._charWidth * horizontalOffset); } if (this._cachedWidth === -1) { this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth; context?.markDidDomLayout(); } return this._cachedWidth; } getWidthIsFast() { return (this.input.lineContent.length < 300 /* Constants.MaxMonospaceDistance */) || this._cachedWidth !== -1; } monospaceAssumptionsAreValid() { if (!this.domNode) { return monospaceAssumptionsAreValid; } if (this.input.lineContent.length < 300 /* Constants.MaxMonospaceDistance */) { const expectedWidth = this.getWidth(null); const actualWidth = this.domNode.domNode.firstChild.offsetWidth; if (Math.abs(expectedWidth - actualWidth) >= 2) { // more than 2px off console.warn(`monospace assumptions have been violated, therefore disabling monospace optimizations!`); monospaceAssumptionsAreValid = false; } } return monospaceAssumptionsAreValid; } toSlowRenderedLine() { return createRenderedLine(this.domNode, this.input, this._characterMapping, false, 0 /* ForeignElementType.None */); } getVisibleRangesForRange(lineNumber, startColumn, endColumn, context) { const startPosition = this._getColumnPixelOffset(lineNumber, startColumn, context); const endPosition = this._getColumnPixelOffset(lineNumber, endColumn, context); return [new FloatHorizontalRange(startPosition, endPosition - startPosition)]; } _getColumnPixelOffset(lineNumber, column, context) { if (column <= 300 /* Constants.MaxMonospaceDistance */) { const horizontalOffset = this._characterMapping.getHorizontalOffset(column); return this._charWidth * horizontalOffset; } const keyColumnOrdinal = Math.floor((column - 1) / 300 /* Constants.MaxMonospaceDistance */) - 1; const keyColumn = (keyColumnOrdinal + 1) * 300 /* Constants.MaxMonospaceDistance */ + 1; let keyColumnPixelOffset = -1; if (this._keyColumnPixelOffsetCache) { keyColumnPixelOffset = this._keyColumnPixelOffsetCache[keyColumnOrdinal]; if (keyColumnPixelOffset === -1) { keyColumnPixelOffset = this._actualReadPixelOffset(lineNumber, keyColumn, context); this._keyColumnPixelOffsetCache[keyColumnOrdinal] = keyColumnPixelOffset; } } if (keyColumnPixelOffset === -1) { // Could not read actual key column pixel offset const horizontalOffset = this._characterMapping.getHorizontalOffset(column); return this._charWidth * horizontalOffset; } const keyColumnHorizontalOffset = this._characterMapping.getHorizontalOffset(keyColumn); const horizontalOffset = this._characterMapping.getHorizontalOffset(column); return keyColumnPixelOffset + this._charWidth * (horizontalOffset - keyColumnHorizontalOffset); } _getReadingTarget(myDomNode) { return myDomNode.domNode.firstChild; } _actualReadPixelOffset(lineNumber, column, context) { if (!this.domNode) { return -1; } const domPosition = this._characterMapping.getDomPosition(column); const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(this.domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context); if (!r || r.length === 0) { return -1; } return r[0].left; } getColumnOfNodeOffset(spanNode, offset) { return getColumnOfNodeOffset(this._characterMapping, spanNode, offset); } } /** * Every time we render a line, we save what we have rendered in an instance of this class. */ class RenderedViewLine { constructor(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements) { this.domNode = domNode; this.input = renderLineInput; this._characterMapping = characterMapping; this._isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent); this._containsForeignElements = containsForeignElements; this._cachedWidth = -1; this._pixelOffsetCache = null; if (!containsRTL || this._characterMapping.length === 0 /* the line is empty */) { this._pixelOffsetCache = new Float32Array(Math.max(2, this._characterMapping.length + 1)); for (let column = 0, len = this._characterMapping.length; column <= len; column++) { this._pixelOffsetCache[column] = -1; } } } // --- Reading from the DOM methods _getReadingTarget(myDomNode) { return myDomNode.domNode.firstChild; } /** * Width of the line in pixels */ getWidth(context) { if (!this.domNode) { return 0; } if (this._cachedWidth === -1) { this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth; context?.markDidDomLayout(); } return this._cachedWidth; } getWidthIsFast() { if (this._cachedWidth === -1) { return false; } return true; } /** * Visible ranges for a model range */ getVisibleRangesForRange(lineNumber, startColumn, endColumn, context) { if (!this.domNode) { return null; } if (this._pixelOffsetCache !== null) { // the text is LTR const startOffset = this._readPixelOffset(this.domNode, lineNumber, startColumn, context); if (startOffset === -1) { return null; } const endOffset = this._readPixelOffset(this.domNode, lineNumber, endColumn, context); if (endOffset === -1) { return null; } return [new FloatHorizontalRange(startOffset, endOffset - startOffset)]; } return this._readVisibleRangesForRange(this.domNode, lineNumber, startColumn, endColumn, context); } _readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context) { if (startColumn === endColumn) { const pixelOffset = this._readPixelOffset(domNode, lineNumber, startColumn, context); if (pixelOffset === -1) { return null; } else { return [new FloatHorizontalRange(pixelOffset, 0)]; } } else { return this._readRawVisibleRangesForRange(domNode, startColumn, endColumn, context); } } _readPixelOffset(domNode, lineNumber, column, context) { if (this._characterMapping.length === 0) { // This line has no content if (this._containsForeignElements === 0 /* ForeignElementType.None */) { // We can assume the line is really empty return 0; } if (this._containsForeignElements === 2 /* ForeignElementType.After */) { // We have foreign elements after the (empty) line return 0; } if (this._containsForeignElements === 1 /* ForeignElementType.Before */) { // We have foreign elements before the (empty) line return this.getWidth(context); } // We have foreign elements before & after the (empty) line const readingTarget = this._getReadingTarget(domNode); if (readingTarget.firstChild) { context.markDidDomLayout(); return readingTarget.firstChild.offsetWidth; } else { return 0; } } if (this._pixelOffsetCache !== null) { // the text is LTR const cachedPixelOffset = this._pixelOffsetCache[column]; if (cachedPixelOffset !== -1) { return cachedPixelOffset; } const result = this._actualReadPixelOffset(domNode, lineNumber, column, context); this._pixelOffsetCache[column] = result; return result; } return this._actualReadPixelOffset(domNode, lineNumber, column, context); } _actualReadPixelOffset(domNode, lineNumber, column, context) { if (this._characterMapping.length === 0) { // This line has no content const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), 0, 0, 0, 0, context); if (!r || r.length === 0) { return -1; } return r[0].left; } if (column === this._characterMapping.length && this._isWhitespaceOnly && this._containsForeignElements === 0 /* ForeignElementType.None */) { // This branch helps in the case of whitespace only lines which have a width set return this.getWidth(context); } const domPosition = this._characterMapping.getDomPosition(column); const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context); if (!r || r.length === 0) { return -1; } const result = r[0].left; if (this.input.isBasicASCII) { const horizontalOffset = this._characterMapping.getHorizontalOffset(column); const expectedResult = Math.round(this.input.spaceWidth * horizontalOffset); if (Math.abs(expectedResult - result) <= 1) { return expectedResult; } } return result; } _readRawVisibleRangesForRange(domNode, startColumn, endColumn, context) { if (startColumn === 1 && endColumn === this._characterMapping.length) { // This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line return [new FloatHorizontalRange(0, this.getWidth(context))]; } const startDomPosition = this._characterMapping.getDomPosition(startColumn); const endDomPosition = this._characterMapping.getDomPosition(endColumn); return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startDomPosition.partIndex, startDomPosition.charIndex, endDomPosition.partIndex, endDomPosition.charIndex, context); } /** * Returns the column for the text found at a specific offset inside a rendered dom node */ getColumnOfNodeOffset(spanNode, offset) { return getColumnOfNodeOffset(this._characterMapping, spanNode, offset); } } class WebKitRenderedViewLine extends RenderedViewLine { _readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context) { const output = super._readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context); if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) { return output; } // WebKit is buggy and returns an expanded range (to contain words in some cases) // The last client rect is enlarged (I think) if (!this.input.containsRTL) { // This is an attempt to patch things up // Find position of last column const endPixelOffset = this._readPixelOffset(domNode, lineNumber, endColumn, context); if (endPixelOffset !== -1) { const lastRange = output[output.length - 1]; if (lastRange.left < endPixelOffset) { // Trim down the width of the last visible range to not go after the last column's position lastRange.width = endPixelOffset - lastRange.left; } } } return output; } } const createRenderedLine = (function () { if (browser.isWebKit) { return createWebKitRenderedLine; } return createNormalRenderedLine; })(); function createWebKitRenderedLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements) { return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } function createNormalRenderedLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements) { return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } export function getColumnOfNodeOffset(characterMapping, spanNode, offset) { const spanNodeTextContentLength = spanNode.textContent.length; let spanIndex = -1; while (spanNode) { spanNode = spanNode.previousSibling; spanIndex++; } return characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength); }