Path: blob/main/src/vs/editor/browser/viewParts/viewLines/rangeUtil.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 { Constants } from '../../../../base/common/uint.js';6import { FloatHorizontalRange } from '../../view/renderingContext.js';7import { DomReadingContext } from './domReadingContext.js';89export class RangeUtil {1011/**12* Reusing the same range here13* because IE is buggy and constantly freezes when using a large number14* of ranges and calling .detach on them15*/16private static _handyReadyRange: Range;1718private static _createRange(): Range {19if (!this._handyReadyRange) {20this._handyReadyRange = document.createRange();21}22return this._handyReadyRange;23}2425private static _detachRange(range: Range, endNode: HTMLElement): void {26// Move range out of the span node, IE doesn't like having many ranges in27// the same spot and will act badly for lines containing dashes ('-')28range.selectNodeContents(endNode);29}3031private static _readClientRects(startElement: Node, startOffset: number, endElement: Node, endOffset: number, endNode: HTMLElement): DOMRectList | null {32const range = this._createRange();33try {34range.setStart(startElement, startOffset);35range.setEnd(endElement, endOffset);3637return range.getClientRects();38} catch (e) {39// This is life ...40return null;41} finally {42this._detachRange(range, endNode);43}44}4546private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): FloatHorizontalRange[] {47if (ranges.length === 1) {48// There is nothing to merge49return ranges;50}5152ranges.sort(FloatHorizontalRange.compare);5354const result: FloatHorizontalRange[] = [];55let resultLen = 0;56let prev = ranges[0];5758for (let i = 1, len = ranges.length; i < len; i++) {59const range = ranges[i];60if (prev.left + prev.width + 0.9 /* account for browser's rounding errors*/ >= range.left) {61prev.width = Math.max(prev.width, range.left + range.width - prev.left);62} else {63result[resultLen++] = prev;64prev = range;65}66}6768result[resultLen++] = prev;6970return result;71}7273private static _createHorizontalRangesFromClientRects(clientRects: DOMRectList | null, clientRectDeltaLeft: number, clientRectScale: number): FloatHorizontalRange[] | null {74if (!clientRects || clientRects.length === 0) {75return null;76}7778// We go through FloatHorizontalRange because it has been observed in bi-di text79// that the clientRects are not coming in sorted from the browser8081const result: FloatHorizontalRange[] = [];82for (let i = 0, len = clientRects.length; i < len; i++) {83const clientRect = clientRects[i];84result[i] = new FloatHorizontalRange(Math.max(0, (clientRect.left - clientRectDeltaLeft) / clientRectScale), clientRect.width / clientRectScale);85}8687return this._mergeAdjacentRanges(result);88}8990public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, context: DomReadingContext): FloatHorizontalRange[] | null {91// Panic check92const min = 0;93const max = domNode.children.length - 1;94if (min > max) {95return null;96}97startChildIndex = Math.min(max, Math.max(min, startChildIndex));98endChildIndex = Math.min(max, Math.max(min, endChildIndex));99100if (startChildIndex === endChildIndex && startOffset === endOffset && startOffset === 0 && !domNode.children[startChildIndex].firstChild) {101// We must find the position at the beginning of a <span>102// To cover cases of empty <span>s, avoid using a range and use the <span>'s bounding box103const clientRects = domNode.children[startChildIndex].getClientRects();104context.markDidDomLayout();105return this._createHorizontalRangesFromClientRects(clientRects, context.clientRectDeltaLeft, context.clientRectScale);106}107108// If crossing over to a span only to select offset 0, then use the previous span's maximum offset109// Chrome is buggy and doesn't handle 0 offsets well sometimes.110if (startChildIndex !== endChildIndex) {111if (endChildIndex > 0 && endOffset === 0) {112endChildIndex--;113endOffset = Constants.MAX_SAFE_SMALL_INTEGER;114}115}116117let startElement = domNode.children[startChildIndex].firstChild;118let endElement = domNode.children[endChildIndex].firstChild;119120if (!startElement || !endElement) {121// When having an empty <span> (without any text content), try to move to the previous <span>122if (!startElement && startOffset === 0 && startChildIndex > 0) {123startElement = domNode.children[startChildIndex - 1].firstChild;124startOffset = Constants.MAX_SAFE_SMALL_INTEGER;125}126if (!endElement && endOffset === 0 && endChildIndex > 0) {127endElement = domNode.children[endChildIndex - 1].firstChild;128endOffset = Constants.MAX_SAFE_SMALL_INTEGER;129}130}131132if (!startElement || !endElement) {133return null;134}135136startOffset = Math.min(startElement.textContent!.length, Math.max(0, startOffset));137endOffset = Math.min(endElement.textContent!.length, Math.max(0, endOffset));138139const clientRects = this._readClientRects(startElement, startOffset, endElement, endOffset, context.endNode);140context.markDidDomLayout();141return this._createHorizontalRangesFromClientRects(clientRects, context.clientRectDeltaLeft, context.clientRectScale);142}143}144145146