@codemirror/view 6.39.13 → 6.39.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 6.39.15 (2026-02-20)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix a regression where the editor would forget previously measured line heights without good reason.
6
+
7
+ Fix an issue where scrolling the cursor into view sometimes wouldn't work on Chrome Android.
8
+
9
+ Fix a bug that broke composition inside of block wrappers.
10
+
11
+ ## 6.39.14 (2026-02-12)
12
+
13
+ ### Bug fixes
14
+
15
+ Improve performance of `posAtCoords` on long lines.
16
+
17
+ Fix a regression where copy and cut in a shadow DOM on Safari would fall back to the native behavior, often copying the wrong text.
18
+
1
19
  ## 6.39.13 (2026-02-08)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -2791,9 +2791,10 @@ class TileUpdate {
2791
2791
  marks.push(tile);
2792
2792
  else if (tile === null || tile === void 0 ? void 0 : tile.isLine())
2793
2793
  line = tile;
2794
+ else if (tile instanceof BlockWrapperTile) ; // Ignore
2794
2795
  else if (parent.nodeName == "DIV" && !line && parent != this.view.contentDOM)
2795
2796
  line = new LineTile(parent, lineBaseAttrs);
2796
- else
2797
+ else if (!line)
2797
2798
  marks.push(MarkTile.of(new MarkDecoration({ tagName: parent.nodeName.toLowerCase(), attributes: getAttrs(parent) }), parent));
2798
2799
  }
2799
2800
  return { line: line, marks };
@@ -3419,6 +3420,19 @@ class DocView {
3419
3420
  };
3420
3421
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3421
3422
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == exports.Direction.LTR);
3423
+ // On mobile browsers, the visual viewport may be smaller than the
3424
+ // actual reported viewport, causing scrollRectIntoView to fail to
3425
+ // scroll properly. Unfortunately, this visual viewport cannot be
3426
+ // updated directly, and scrollIntoView is the only way a script
3427
+ // can affect it. So this tries to kludge around the problem by
3428
+ // calling scrollIntoView on the scroll target's line.
3429
+ if (window.visualViewport && window.innerHeight - window.visualViewport.height > 1 &&
3430
+ (rect.top > window.pageYOffset + window.visualViewport.offsetTop + window.visualViewport.height ||
3431
+ rect.bottom < window.pageYOffset + window.visualViewport.offsetTop)) {
3432
+ let line = this.view.docView.lineAt(range.head, 1);
3433
+ if (line)
3434
+ line.dom.scrollIntoView({ block: "nearest" });
3435
+ }
3422
3436
  }
3423
3437
  lineHasWidget(pos) {
3424
3438
  let scan = (child) => child.isWidget() || child.children.some(scan);
@@ -3767,86 +3781,164 @@ function posAtCoords(view, coords, precise, scanY) {
3767
3781
  let line = view.docView.lineAt(block.from, 2);
3768
3782
  if (!line || line.length != block.length)
3769
3783
  line = view.docView.lineAt(block.from, -2);
3770
- return posAtCoordsInline(view, line, block.from, x, y);
3784
+ return new InlineCoordsScan(view, x, y, view.textDirectionAt(block.from)).scanTile(line, block.from);
3771
3785
  }
3772
- // Scan through the rectangles for the content of a tile, finding the
3773
- // one closest to the given coordinates, prefering closeness in Y over
3774
- // closeness in X.
3775
- //
3776
- // If this is a text tile, go character-by-character. For line or mark
3777
- // tiles, check each non-point-widget child, and descend text or mark
3778
- // tiles with a recursive call.
3779
- //
3780
- // For non-wrapped, purely left-to-right text, this could use a binary
3781
- // search. But because this seems to be fast enough, for how often it
3782
- // is called, there's not currently a specialized implementation for
3783
- // that.
3784
- function posAtCoordsInline(view, tile, offset, x, y) {
3785
- let closest = -1, closestRect = null;
3786
- let dxClosest = 1e9, dyClosest = 1e9;
3787
- let rowTop = y, rowBot = y;
3788
- let checkRects = (rects, index) => {
3789
- for (let i = 0; i < rects.length; i++) {
3790
- let rect = rects[i];
3791
- if (rect.top == rect.bottom)
3792
- continue;
3793
- let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3794
- let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3795
- if (rect.top <= rowBot && rect.bottom >= rowTop) {
3796
- // Rectangle is in the current row
3797
- rowTop = Math.min(rect.top, rowTop);
3798
- rowBot = Math.max(rect.bottom, rowBot);
3799
- dy = 0;
3800
- }
3801
- if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3802
- if (closest >= 0 && dyClosest && dxClosest < dx &&
3803
- closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3804
- // Retroactively set dy to 0 if the current match is in this row.
3805
- dyClosest = 0;
3806
- }
3807
- else {
3808
- closest = index;
3809
- dxClosest = dx;
3810
- dyClosest = dy;
3811
- closestRect = rect;
3786
+ class InlineCoordsScan {
3787
+ constructor(view, x, y, baseDir) {
3788
+ this.view = view;
3789
+ this.x = x;
3790
+ this.y = y;
3791
+ this.baseDir = baseDir;
3792
+ // Cached bidi info
3793
+ this.line = null;
3794
+ this.spans = null;
3795
+ }
3796
+ bidiSpansAt(pos) {
3797
+ if (!this.line || this.line.from > pos || this.line.to < pos) {
3798
+ this.line = this.view.state.doc.lineAt(pos);
3799
+ this.spans = this.view.bidiSpans(this.line);
3800
+ }
3801
+ return this;
3802
+ }
3803
+ baseDirAt(pos, side) {
3804
+ let { line, spans } = this.bidiSpansAt(pos);
3805
+ let level = spans[BidiSpan.find(spans, pos - line.from, -1, side)].level;
3806
+ return level == this.baseDir;
3807
+ }
3808
+ dirAt(pos, side) {
3809
+ let { line, spans } = this.bidiSpansAt(pos);
3810
+ return spans[BidiSpan.find(spans, pos - line.from, -1, side)].dir;
3811
+ }
3812
+ // Used to short-circuit bidi tests for content with a uniform direction
3813
+ bidiIn(from, to) {
3814
+ let { spans, line } = this.bidiSpansAt(from);
3815
+ return spans.length > 1 || spans.length && (spans[0].level != this.baseDir || spans[0].to + line.from < to);
3816
+ }
3817
+ // Scan through the rectangles for the content of a tile with inline
3818
+ // content, looking for one that overlaps the queried position
3819
+ // vertically andis
3820
+ // closest horizontally. The caller is responsible for dividing its
3821
+ // content into N pieces, and pass an array with N+1 positions
3822
+ // (including the position after the last piece). For a text tile,
3823
+ // these will be character clusters, for a composite tile, these
3824
+ // will be child tiles.
3825
+ scan(positions, getRects) {
3826
+ let lo = 0, hi = positions.length - 1, seen = new Set();
3827
+ let bidi = this.bidiIn(positions[0], positions[hi]);
3828
+ let above, below;
3829
+ let closestI = -1, closestDx = 1e9, closestRect;
3830
+ // Because, when the content is bidirectional, a regular binary
3831
+ // search is hard to perform (the content order does not
3832
+ // correspond to visual order), this loop does something between a
3833
+ // regular binary search and a full scan, depending on what it can
3834
+ // get away with. The outer hi/lo bounds are only adjusted for
3835
+ // elements that are part of the base order.
3836
+ //
3837
+ // To make sure all elements inside those bounds are visited,
3838
+ // eventually, we keep a set of seen indices, and if the midpoint
3839
+ // has already been handled, we start in a random index within the
3840
+ // current bounds and scan forward until we find an index that
3841
+ // hasn't been seen yet.
3842
+ search: while (lo < hi) {
3843
+ let dist = hi - lo, mid = (lo + hi) >> 1;
3844
+ adjust: if (seen.has(mid)) {
3845
+ let scan = lo + Math.floor(Math.random() * dist);
3846
+ for (let i = 0; i < dist; i++) {
3847
+ if (!seen.has(scan)) {
3848
+ mid = scan;
3849
+ break adjust;
3850
+ }
3851
+ scan++;
3852
+ if (scan == hi)
3853
+ scan = lo; // Wrap around
3812
3854
  }
3855
+ break search; // No index found, we're done
3813
3856
  }
3857
+ seen.add(mid);
3858
+ let rects = getRects(mid);
3859
+ if (rects)
3860
+ for (let i = 0; i < rects.length; i++) {
3861
+ let rect = rects[i], side = 0;
3862
+ if (rect.bottom < this.y) {
3863
+ if (!above || above.bottom < rect.bottom)
3864
+ above = rect;
3865
+ side = 1;
3866
+ }
3867
+ else if (rect.top > this.y) {
3868
+ if (!below || below.top > rect.top)
3869
+ below = rect;
3870
+ side = -1;
3871
+ }
3872
+ else {
3873
+ let off = rect.left > this.x ? this.x - rect.left : rect.right < this.x ? this.x - rect.right : 0;
3874
+ let dx = Math.abs(off);
3875
+ if (dx < closestDx) {
3876
+ closestI = mid;
3877
+ closestDx = dx;
3878
+ closestRect = rect;
3879
+ }
3880
+ if (off)
3881
+ side = (off < 0) == (this.baseDir == exports.Direction.LTR) ? -1 : 1;
3882
+ }
3883
+ // Narrow binary search when it is safe to do so
3884
+ if (side == -1 && (!bidi || this.baseDirAt(positions[mid], 1)))
3885
+ hi = mid;
3886
+ else if (side == 1 && (!bidi || this.baseDirAt(positions[mid + 1], -1)))
3887
+ lo = mid + 1;
3888
+ }
3814
3889
  }
3815
- };
3816
- if (tile.isText()) {
3817
- for (let i = 0; i < tile.length;) {
3818
- let next = state.findClusterBreak(tile.text, i);
3819
- checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3820
- if (!dxClosest && !dyClosest)
3821
- break;
3822
- i = next;
3890
+ // If no element with y overlap is found, find the nearest element
3891
+ // on the y axis, move this.y into it, and retry the scan.
3892
+ if (!closestRect) {
3893
+ let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3894
+ this.y = (side.top + side.bottom) / 2;
3895
+ return this.scan(positions, getRects);
3823
3896
  }
3824
- let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3825
- return after ? new PosAssoc(offset + state.findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3897
+ let ltr = (bidi ? this.dirAt(positions[closestI], 1) : this.baseDir) == exports.Direction.LTR;
3898
+ return {
3899
+ i: closestI,
3900
+ // Test whether x is closes to the start or end of this element
3901
+ after: (this.x > (closestRect.left + closestRect.right) / 2) == ltr
3902
+ };
3826
3903
  }
3827
- else {
3904
+ scanText(tile, offset) {
3905
+ let positions = [];
3906
+ for (let i = 0; i < tile.length; i = state.findClusterBreak(tile.text, i))
3907
+ positions.push(offset + i);
3908
+ positions.push(offset + tile.length);
3909
+ let scan = this.scan(positions, i => {
3910
+ let off = positions[i] - offset, end = positions[i + 1] - offset;
3911
+ return textRange(tile.dom, off, end).getClientRects();
3912
+ });
3913
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(positions[scan.i], 1);
3914
+ }
3915
+ scanTile(tile, offset) {
3828
3916
  if (!tile.length)
3829
3917
  return new PosAssoc(offset, 1);
3830
- for (let i = 0; i < tile.children.length; i++) {
3918
+ if (tile.children.length == 1) { // Short-circuit single-child tiles
3919
+ let child = tile.children[0];
3920
+ if (child.isText())
3921
+ return this.scanText(child, offset);
3922
+ else if (child.isComposite())
3923
+ return this.scanTile(child, offset);
3924
+ }
3925
+ let positions = [offset];
3926
+ for (let i = 0, pos = offset; i < tile.children.length; i++)
3927
+ positions.push(pos += tile.children[i].length);
3928
+ let scan = this.scan(positions, i => {
3831
3929
  let child = tile.children[i];
3832
3930
  if (child.flags & 48 /* TileFlag.PointWidget */)
3833
- continue;
3834
- let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3835
- checkRects(rects, i);
3836
- if (!dxClosest && !dyClosest)
3837
- break;
3838
- }
3839
- let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3840
- if (inner.isComposite() || inner.isText())
3841
- return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3842
- let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3843
- return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3931
+ return null;
3932
+ return (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3933
+ });
3934
+ let child = tile.children[scan.i], pos = positions[scan.i];
3935
+ if (child.isText())
3936
+ return this.scanText(child, pos);
3937
+ if (child.isComposite())
3938
+ return this.scanTile(child, pos);
3939
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(pos, 1);
3844
3940
  }
3845
3941
  }
3846
- function dirAt(view, pos) {
3847
- let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3848
- return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3849
- }
3850
3942
 
3851
3943
  const LineBreakPlaceholder = "\uffff";
3852
3944
  class DOMReader {
@@ -5000,8 +5092,7 @@ handlers.copy = handlers.cut = (view, event) => {
5000
5092
  // spans multiple elements including this CodeMirror. The copy event may
5001
5093
  // bubble through CodeMirror (e.g. when CodeMirror is the first or the last
5002
5094
  // element in the selection), but we should let the parent handle it.
5003
- let domSel = getSelection(view.root);
5004
- if (domSel && !hasSelection(view.contentDOM, domSel))
5095
+ if (!hasSelection(view.contentDOM, view.observer.selectionRange))
5005
5096
  return false;
5006
5097
  let { text, ranges, linewise } = copiedRange(view.state);
5007
5098
  if (!text && !linewise)
@@ -6125,7 +6216,7 @@ class ViewState {
6125
6216
  let oracle = this.heightOracle;
6126
6217
  let whiteSpace = style.whiteSpace;
6127
6218
  this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
6128
- let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent;
6219
+ let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent === "refresh";
6129
6220
  let domRect = dom.getBoundingClientRect();
6130
6221
  let measureContent = refresh || this.mustMeasureContent || this.contentDOMHeight != domRect.height;
6131
6222
  this.contentDOMHeight = domRect.height;
@@ -7702,7 +7793,7 @@ class EditorView {
7702
7793
  this.requestMeasure();
7703
7794
  if ((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)
7704
7795
  document.fonts.ready.then(() => {
7705
- this.viewState.mustMeasureContent = true;
7796
+ this.viewState.mustMeasureContent = "refresh";
7706
7797
  this.requestMeasure();
7707
7798
  });
7708
7799
  }
package/dist/index.js CHANGED
@@ -2787,9 +2787,10 @@ class TileUpdate {
2787
2787
  marks.push(tile);
2788
2788
  else if (tile === null || tile === void 0 ? void 0 : tile.isLine())
2789
2789
  line = tile;
2790
+ else if (tile instanceof BlockWrapperTile) ; // Ignore
2790
2791
  else if (parent.nodeName == "DIV" && !line && parent != this.view.contentDOM)
2791
2792
  line = new LineTile(parent, lineBaseAttrs);
2792
- else
2793
+ else if (!line)
2793
2794
  marks.push(MarkTile.of(new MarkDecoration({ tagName: parent.nodeName.toLowerCase(), attributes: getAttrs(parent) }), parent));
2794
2795
  }
2795
2796
  return { line: line, marks };
@@ -3415,6 +3416,19 @@ class DocView {
3415
3416
  };
3416
3417
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3417
3418
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == Direction.LTR);
3419
+ // On mobile browsers, the visual viewport may be smaller than the
3420
+ // actual reported viewport, causing scrollRectIntoView to fail to
3421
+ // scroll properly. Unfortunately, this visual viewport cannot be
3422
+ // updated directly, and scrollIntoView is the only way a script
3423
+ // can affect it. So this tries to kludge around the problem by
3424
+ // calling scrollIntoView on the scroll target's line.
3425
+ if (window.visualViewport && window.innerHeight - window.visualViewport.height > 1 &&
3426
+ (rect.top > window.pageYOffset + window.visualViewport.offsetTop + window.visualViewport.height ||
3427
+ rect.bottom < window.pageYOffset + window.visualViewport.offsetTop)) {
3428
+ let line = this.view.docView.lineAt(range.head, 1);
3429
+ if (line)
3430
+ line.dom.scrollIntoView({ block: "nearest" });
3431
+ }
3418
3432
  }
3419
3433
  lineHasWidget(pos) {
3420
3434
  let scan = (child) => child.isWidget() || child.children.some(scan);
@@ -3763,86 +3777,164 @@ function posAtCoords(view, coords, precise, scanY) {
3763
3777
  let line = view.docView.lineAt(block.from, 2);
3764
3778
  if (!line || line.length != block.length)
3765
3779
  line = view.docView.lineAt(block.from, -2);
3766
- return posAtCoordsInline(view, line, block.from, x, y);
3780
+ return new InlineCoordsScan(view, x, y, view.textDirectionAt(block.from)).scanTile(line, block.from);
3767
3781
  }
3768
- // Scan through the rectangles for the content of a tile, finding the
3769
- // one closest to the given coordinates, prefering closeness in Y over
3770
- // closeness in X.
3771
- //
3772
- // If this is a text tile, go character-by-character. For line or mark
3773
- // tiles, check each non-point-widget child, and descend text or mark
3774
- // tiles with a recursive call.
3775
- //
3776
- // For non-wrapped, purely left-to-right text, this could use a binary
3777
- // search. But because this seems to be fast enough, for how often it
3778
- // is called, there's not currently a specialized implementation for
3779
- // that.
3780
- function posAtCoordsInline(view, tile, offset, x, y) {
3781
- let closest = -1, closestRect = null;
3782
- let dxClosest = 1e9, dyClosest = 1e9;
3783
- let rowTop = y, rowBot = y;
3784
- let checkRects = (rects, index) => {
3785
- for (let i = 0; i < rects.length; i++) {
3786
- let rect = rects[i];
3787
- if (rect.top == rect.bottom)
3788
- continue;
3789
- let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3790
- let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3791
- if (rect.top <= rowBot && rect.bottom >= rowTop) {
3792
- // Rectangle is in the current row
3793
- rowTop = Math.min(rect.top, rowTop);
3794
- rowBot = Math.max(rect.bottom, rowBot);
3795
- dy = 0;
3796
- }
3797
- if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3798
- if (closest >= 0 && dyClosest && dxClosest < dx &&
3799
- closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3800
- // Retroactively set dy to 0 if the current match is in this row.
3801
- dyClosest = 0;
3802
- }
3803
- else {
3804
- closest = index;
3805
- dxClosest = dx;
3806
- dyClosest = dy;
3807
- closestRect = rect;
3782
+ class InlineCoordsScan {
3783
+ constructor(view, x, y, baseDir) {
3784
+ this.view = view;
3785
+ this.x = x;
3786
+ this.y = y;
3787
+ this.baseDir = baseDir;
3788
+ // Cached bidi info
3789
+ this.line = null;
3790
+ this.spans = null;
3791
+ }
3792
+ bidiSpansAt(pos) {
3793
+ if (!this.line || this.line.from > pos || this.line.to < pos) {
3794
+ this.line = this.view.state.doc.lineAt(pos);
3795
+ this.spans = this.view.bidiSpans(this.line);
3796
+ }
3797
+ return this;
3798
+ }
3799
+ baseDirAt(pos, side) {
3800
+ let { line, spans } = this.bidiSpansAt(pos);
3801
+ let level = spans[BidiSpan.find(spans, pos - line.from, -1, side)].level;
3802
+ return level == this.baseDir;
3803
+ }
3804
+ dirAt(pos, side) {
3805
+ let { line, spans } = this.bidiSpansAt(pos);
3806
+ return spans[BidiSpan.find(spans, pos - line.from, -1, side)].dir;
3807
+ }
3808
+ // Used to short-circuit bidi tests for content with a uniform direction
3809
+ bidiIn(from, to) {
3810
+ let { spans, line } = this.bidiSpansAt(from);
3811
+ return spans.length > 1 || spans.length && (spans[0].level != this.baseDir || spans[0].to + line.from < to);
3812
+ }
3813
+ // Scan through the rectangles for the content of a tile with inline
3814
+ // content, looking for one that overlaps the queried position
3815
+ // vertically andis
3816
+ // closest horizontally. The caller is responsible for dividing its
3817
+ // content into N pieces, and pass an array with N+1 positions
3818
+ // (including the position after the last piece). For a text tile,
3819
+ // these will be character clusters, for a composite tile, these
3820
+ // will be child tiles.
3821
+ scan(positions, getRects) {
3822
+ let lo = 0, hi = positions.length - 1, seen = new Set();
3823
+ let bidi = this.bidiIn(positions[0], positions[hi]);
3824
+ let above, below;
3825
+ let closestI = -1, closestDx = 1e9, closestRect;
3826
+ // Because, when the content is bidirectional, a regular binary
3827
+ // search is hard to perform (the content order does not
3828
+ // correspond to visual order), this loop does something between a
3829
+ // regular binary search and a full scan, depending on what it can
3830
+ // get away with. The outer hi/lo bounds are only adjusted for
3831
+ // elements that are part of the base order.
3832
+ //
3833
+ // To make sure all elements inside those bounds are visited,
3834
+ // eventually, we keep a set of seen indices, and if the midpoint
3835
+ // has already been handled, we start in a random index within the
3836
+ // current bounds and scan forward until we find an index that
3837
+ // hasn't been seen yet.
3838
+ search: while (lo < hi) {
3839
+ let dist = hi - lo, mid = (lo + hi) >> 1;
3840
+ adjust: if (seen.has(mid)) {
3841
+ let scan = lo + Math.floor(Math.random() * dist);
3842
+ for (let i = 0; i < dist; i++) {
3843
+ if (!seen.has(scan)) {
3844
+ mid = scan;
3845
+ break adjust;
3846
+ }
3847
+ scan++;
3848
+ if (scan == hi)
3849
+ scan = lo; // Wrap around
3808
3850
  }
3851
+ break search; // No index found, we're done
3809
3852
  }
3853
+ seen.add(mid);
3854
+ let rects = getRects(mid);
3855
+ if (rects)
3856
+ for (let i = 0; i < rects.length; i++) {
3857
+ let rect = rects[i], side = 0;
3858
+ if (rect.bottom < this.y) {
3859
+ if (!above || above.bottom < rect.bottom)
3860
+ above = rect;
3861
+ side = 1;
3862
+ }
3863
+ else if (rect.top > this.y) {
3864
+ if (!below || below.top > rect.top)
3865
+ below = rect;
3866
+ side = -1;
3867
+ }
3868
+ else {
3869
+ let off = rect.left > this.x ? this.x - rect.left : rect.right < this.x ? this.x - rect.right : 0;
3870
+ let dx = Math.abs(off);
3871
+ if (dx < closestDx) {
3872
+ closestI = mid;
3873
+ closestDx = dx;
3874
+ closestRect = rect;
3875
+ }
3876
+ if (off)
3877
+ side = (off < 0) == (this.baseDir == Direction.LTR) ? -1 : 1;
3878
+ }
3879
+ // Narrow binary search when it is safe to do so
3880
+ if (side == -1 && (!bidi || this.baseDirAt(positions[mid], 1)))
3881
+ hi = mid;
3882
+ else if (side == 1 && (!bidi || this.baseDirAt(positions[mid + 1], -1)))
3883
+ lo = mid + 1;
3884
+ }
3810
3885
  }
3811
- };
3812
- if (tile.isText()) {
3813
- for (let i = 0; i < tile.length;) {
3814
- let next = findClusterBreak(tile.text, i);
3815
- checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3816
- if (!dxClosest && !dyClosest)
3817
- break;
3818
- i = next;
3886
+ // If no element with y overlap is found, find the nearest element
3887
+ // on the y axis, move this.y into it, and retry the scan.
3888
+ if (!closestRect) {
3889
+ let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3890
+ this.y = (side.top + side.bottom) / 2;
3891
+ return this.scan(positions, getRects);
3819
3892
  }
3820
- let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3821
- return after ? new PosAssoc(offset + findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3893
+ let ltr = (bidi ? this.dirAt(positions[closestI], 1) : this.baseDir) == Direction.LTR;
3894
+ return {
3895
+ i: closestI,
3896
+ // Test whether x is closes to the start or end of this element
3897
+ after: (this.x > (closestRect.left + closestRect.right) / 2) == ltr
3898
+ };
3822
3899
  }
3823
- else {
3900
+ scanText(tile, offset) {
3901
+ let positions = [];
3902
+ for (let i = 0; i < tile.length; i = findClusterBreak(tile.text, i))
3903
+ positions.push(offset + i);
3904
+ positions.push(offset + tile.length);
3905
+ let scan = this.scan(positions, i => {
3906
+ let off = positions[i] - offset, end = positions[i + 1] - offset;
3907
+ return textRange(tile.dom, off, end).getClientRects();
3908
+ });
3909
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(positions[scan.i], 1);
3910
+ }
3911
+ scanTile(tile, offset) {
3824
3912
  if (!tile.length)
3825
3913
  return new PosAssoc(offset, 1);
3826
- for (let i = 0; i < tile.children.length; i++) {
3914
+ if (tile.children.length == 1) { // Short-circuit single-child tiles
3915
+ let child = tile.children[0];
3916
+ if (child.isText())
3917
+ return this.scanText(child, offset);
3918
+ else if (child.isComposite())
3919
+ return this.scanTile(child, offset);
3920
+ }
3921
+ let positions = [offset];
3922
+ for (let i = 0, pos = offset; i < tile.children.length; i++)
3923
+ positions.push(pos += tile.children[i].length);
3924
+ let scan = this.scan(positions, i => {
3827
3925
  let child = tile.children[i];
3828
3926
  if (child.flags & 48 /* TileFlag.PointWidget */)
3829
- continue;
3830
- let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3831
- checkRects(rects, i);
3832
- if (!dxClosest && !dyClosest)
3833
- break;
3834
- }
3835
- let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3836
- if (inner.isComposite() || inner.isText())
3837
- return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3838
- let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3839
- return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3927
+ return null;
3928
+ return (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3929
+ });
3930
+ let child = tile.children[scan.i], pos = positions[scan.i];
3931
+ if (child.isText())
3932
+ return this.scanText(child, pos);
3933
+ if (child.isComposite())
3934
+ return this.scanTile(child, pos);
3935
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(pos, 1);
3840
3936
  }
3841
3937
  }
3842
- function dirAt(view, pos) {
3843
- let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3844
- return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3845
- }
3846
3938
 
3847
3939
  const LineBreakPlaceholder = "\uffff";
3848
3940
  class DOMReader {
@@ -4996,8 +5088,7 @@ handlers.copy = handlers.cut = (view, event) => {
4996
5088
  // spans multiple elements including this CodeMirror. The copy event may
4997
5089
  // bubble through CodeMirror (e.g. when CodeMirror is the first or the last
4998
5090
  // element in the selection), but we should let the parent handle it.
4999
- let domSel = getSelection(view.root);
5000
- if (domSel && !hasSelection(view.contentDOM, domSel))
5091
+ if (!hasSelection(view.contentDOM, view.observer.selectionRange))
5001
5092
  return false;
5002
5093
  let { text, ranges, linewise } = copiedRange(view.state);
5003
5094
  if (!text && !linewise)
@@ -6120,7 +6211,7 @@ class ViewState {
6120
6211
  let oracle = this.heightOracle;
6121
6212
  let whiteSpace = style.whiteSpace;
6122
6213
  this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
6123
- let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent;
6214
+ let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent === "refresh";
6124
6215
  let domRect = dom.getBoundingClientRect();
6125
6216
  let measureContent = refresh || this.mustMeasureContent || this.contentDOMHeight != domRect.height;
6126
6217
  this.contentDOMHeight = domRect.height;
@@ -7697,7 +7788,7 @@ class EditorView {
7697
7788
  this.requestMeasure();
7698
7789
  if ((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)
7699
7790
  document.fonts.ready.then(() => {
7700
- this.viewState.mustMeasureContent = true;
7791
+ this.viewState.mustMeasureContent = "refresh";
7701
7792
  this.requestMeasure();
7702
7793
  });
7703
7794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.13",
3
+ "version": "6.39.15",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",