| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- /*---------------------------------------------------------------------------------------------
- * 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 DomReadingContext {
- constructor(domNode, endNode) {
- this._domNode = domNode;
- this._clientRectDeltaLeft = 0;
- this._clientRectScale = 1;
- this._clientRectRead = false;
- this.endNode = endNode;
- }
- readClientRect() {
- if (!this._clientRectRead) {
- this._clientRectRead = true;
- const rect = this._domNode.getBoundingClientRect();
- this._clientRectDeltaLeft = rect.left;
- this._clientRectScale = rect.width / this._domNode.offsetWidth;
- }
- }
- get clientRectDeltaLeft() {
- if (!this._clientRectRead) {
- this.readClientRect();
- }
- return this._clientRectDeltaLeft;
- }
- get clientRectScale() {
- if (!this._clientRectRead) {
- this.readClientRect();
- }
- return this._clientRectScale;
- }
- }
- export class ViewLineOptions {
- constructor(config, themeType) {
- this.themeType = themeType;
- const options = config.options;
- const fontInfo = options.get(46 /* EditorOption.fontInfo */);
- this.renderWhitespace = options.get(90 /* EditorOption.renderWhitespace */);
- this.renderControlCharacters = options.get(85 /* EditorOption.renderControlCharacters */);
- this.spaceWidth = fontInfo.spaceWidth;
- this.middotWidth = fontInfo.middotWidth;
- this.wsmiddotWidth = fontInfo.wsmiddotWidth;
- this.useMonospaceOptimizations = (fontInfo.isMonospace
- && !options.get(29 /* EditorOption.disableMonospaceOptimizations */));
- this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow;
- this.lineHeight = options.get(61 /* EditorOption.lineHeight */);
- this.stopRenderingLineAfter = options.get(107 /* EditorOption.stopRenderingLineAfter */);
- this.fontLigatures = options.get(47 /* 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 {
- 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, 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) || this._options.renderWhitespace !== 'selection') {
- actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', 0 /* InlineDecorationType.Regular */));
- }
- else {
- 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.appendASCIIString('<div style="top:');
- sb.appendASCIIString(String(deltaTop));
- sb.appendASCIIString('px;height:');
- sb.appendASCIIString(String(this._options.lineHeight));
- sb.appendASCIIString('px;" class="');
- sb.appendASCIIString(ViewLine.CLASS_NAME);
- sb.appendASCIIString('">');
- const output = renderViewLine(renderLineInput, sb);
- sb.appendASCIIString('</div>');
- let renderedViewLine = null;
- if (monospaceAssumptionsAreValid && canUseFastRenderedViewLine && lineData.isBasicASCII && options.useMonospaceOptimizations && output.containsForeignElements === 0 /* ForeignElementType.None */) {
- if (lineData.content.length < 300 && renderLineInput.lineTokens.getCount() < 100) {
- // Browser rounding errors have been observed in Chrome and IE, so using the fast
- // view line only for short lines. Please test before removing the length check...
- // ---
- // Another rounding error has been observed on Linux in VSCode, where <span> width
- // rounding errors add up to an observable large number...
- // ---
- // Also see another example of rounding errors on Windows in
- // https://github.com/microsoft/vscode/issues/33178
- 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) {
- if (this._renderedViewLine && this._renderedViewLine.domNode) {
- this._renderedViewLine.domNode.setTop(deltaTop);
- this._renderedViewLine.domNode.setHeight(this._options.lineHeight);
- }
- }
- // --- end IVisibleLineData
- getWidth() {
- if (!this._renderedViewLine) {
- return 0;
- }
- return this._renderedViewLine.getWidth();
- }
- 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;
- let outsideRenderedLine = false;
- if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter + 1 && endColumn > stopRenderingLineAfter + 1) {
- // This range is obviously not visible
- outsideRenderedLine = true;
- }
- 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(outsideRenderedLine, horizontalRanges);
- }
- return null;
- }
- getColumnOfNodeOffset(lineNumber, spanNode, offset) {
- if (!this._renderedViewLine) {
- return 1;
- }
- return this._renderedViewLine.getColumnOfNodeOffset(lineNumber, spanNode, offset);
- }
- }
- ViewLine.CLASS_NAME = 'view-line';
- /**
- * 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.domNode = domNode;
- this.input = renderLineInput;
- this._characterMapping = characterMapping;
- this._charWidth = renderLineInput.spaceWidth;
- }
- getWidth() {
- return Math.round(this._getCharPosition(this._characterMapping.length));
- }
- getWidthIsFast() {
- return true;
- }
- monospaceAssumptionsAreValid() {
- if (!this.domNode) {
- return monospaceAssumptionsAreValid;
- }
- const expectedWidth = this.getWidth();
- 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._getCharPosition(startColumn);
- const endPosition = this._getCharPosition(endColumn);
- return [new FloatHorizontalRange(startPosition, endPosition - startPosition)];
- }
- _getCharPosition(column) {
- const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
- return this._charWidth * horizontalOffset;
- }
- getColumnOfNodeOffset(lineNumber, spanNode, offset) {
- const spanNodeTextContentLength = spanNode.textContent.length;
- let spanIndex = -1;
- while (spanNode) {
- spanNode = spanNode.previousSibling;
- spanIndex++;
- }
- return this._characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength);
- }
- }
- /**
- * 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() {
- if (!this.domNode) {
- return 0;
- }
- if (this._cachedWidth === -1) {
- this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth;
- }
- 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();
- }
- // We have foreign elements before & after the (empty) line
- const readingTarget = this._getReadingTarget(domNode);
- if (readingTarget.firstChild) {
- 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.clientRectDeltaLeft, context.clientRectScale, context.endNode);
- 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();
- }
- const domPosition = this._characterMapping.getDomPosition(column);
- const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context.clientRectDeltaLeft, context.clientRectScale, context.endNode);
- 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())];
- }
- 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.clientRectDeltaLeft, context.clientRectScale, context.endNode);
- }
- /**
- * Returns the column for the text found at a specific offset inside a rendered dom node
- */
- getColumnOfNodeOffset(lineNumber, spanNode, offset) {
- const spanNodeTextContentLength = spanNode.textContent.length;
- let spanIndex = -1;
- while (spanNode) {
- spanNode = spanNode.previousSibling;
- spanIndex++;
- }
- return this._characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength);
- }
- }
- 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);
- }
|