@codemirror/view 6.39.12 → 6.39.14

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,19 @@
1
+ ## 6.39.14 (2026-02-12)
2
+
3
+ ### Bug fixes
4
+
5
+ Improve performance of `posAtCoords` on long lines.
6
+
7
+ 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.
8
+
9
+ ## 6.39.13 (2026-02-08)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix an issue where a widget at start or end of line, when wrapped to cover that whole line, could block vertical cursor motion.
14
+
15
+ Fix an issue `EditorView.moveVertically` that would sometimes cause selection-extending vertical motion to get stuck on line wrapping points.
16
+
1
17
  ## 6.39.12 (2026-01-30)
2
18
 
3
19
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -3660,7 +3660,7 @@ function moveVertically(view, start, forward, distance) {
3660
3660
  return state.EditorSelection.cursor(startPos, start.assoc);
3661
3661
  let goal = start.goalColumn, startY;
3662
3662
  let rect = view.contentDOM.getBoundingClientRect();
3663
- let startCoords = view.coordsAtPos(startPos, start.assoc || -1), docTop = view.documentTop;
3663
+ let startCoords = view.coordsAtPos(startPos, (start.empty ? start.assoc : 0) || (forward ? 1 : -1)), docTop = view.documentTop;
3664
3664
  if (startCoords) {
3665
3665
  if (goal == null)
3666
3666
  goal = startCoords.left - rect.left;
@@ -3744,7 +3744,7 @@ function posAtCoords(view, coords, precise, scanY) {
3744
3744
  if (scanY < 0 ? block.to < view.viewport.from : block.from > view.viewport.to)
3745
3745
  break;
3746
3746
  // Check whether we aren't landing on the top/bottom padding of the line
3747
- let rect = view.docView.coordsAt(scanY < 0 ? block.from : block.to, scanY);
3747
+ let rect = view.docView.coordsAt(scanY < 0 ? block.from : block.to, scanY > 0 ? -1 : 1);
3748
3748
  if (rect && (scanY < 0 ? rect.top <= yOffset + docTop : rect.bottom >= yOffset + docTop))
3749
3749
  break;
3750
3750
  }
@@ -3767,86 +3767,164 @@ function posAtCoords(view, coords, precise, scanY) {
3767
3767
  let line = view.docView.lineAt(block.from, 2);
3768
3768
  if (!line || line.length != block.length)
3769
3769
  line = view.docView.lineAt(block.from, -2);
3770
- return posAtCoordsInline(view, line, block.from, x, y);
3770
+ return new InlineCoordsScan(view, x, y, view.textDirectionAt(block.from)).scanTile(line, block.from);
3771
3771
  }
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;
3772
+ class InlineCoordsScan {
3773
+ constructor(view, x, y, baseDir) {
3774
+ this.view = view;
3775
+ this.x = x;
3776
+ this.y = y;
3777
+ this.baseDir = baseDir;
3778
+ // Cached bidi info
3779
+ this.line = null;
3780
+ this.spans = null;
3781
+ }
3782
+ bidiSpansAt(pos) {
3783
+ if (!this.line || this.line.from > pos || this.line.to < pos) {
3784
+ this.line = this.view.state.doc.lineAt(pos);
3785
+ this.spans = this.view.bidiSpans(this.line);
3786
+ }
3787
+ return this;
3788
+ }
3789
+ baseDirAt(pos, side) {
3790
+ let { line, spans } = this.bidiSpansAt(pos);
3791
+ let level = spans[BidiSpan.find(spans, pos - line.from, -1, side)].level;
3792
+ return level == this.baseDir;
3793
+ }
3794
+ dirAt(pos, side) {
3795
+ let { line, spans } = this.bidiSpansAt(pos);
3796
+ return spans[BidiSpan.find(spans, pos - line.from, -1, side)].dir;
3797
+ }
3798
+ // Used to short-circuit bidi tests for content with a uniform direction
3799
+ bidiIn(from, to) {
3800
+ let { spans, line } = this.bidiSpansAt(from);
3801
+ return spans.length > 1 || spans.length && (spans[0].level != this.baseDir || spans[0].to + line.from < to);
3802
+ }
3803
+ // Scan through the rectangles for the content of a tile with inline
3804
+ // content, looking for one that overlaps the queried position
3805
+ // vertically andis
3806
+ // closest horizontally. The caller is responsible for dividing its
3807
+ // content into N pieces, and pass an array with N+1 positions
3808
+ // (including the position after the last piece). For a text tile,
3809
+ // these will be character clusters, for a composite tile, these
3810
+ // will be child tiles.
3811
+ scan(positions, getRects) {
3812
+ let lo = 0, hi = positions.length - 1, seen = new Set();
3813
+ let bidi = this.bidiIn(positions[0], positions[hi]);
3814
+ let above, below;
3815
+ let closestI = -1, closestDx = 1e9, closestRect;
3816
+ // Because, when the content is bidirectional, a regular binary
3817
+ // search is hard to perform (the content order does not
3818
+ // correspond to visual order), this loop does something between a
3819
+ // regular binary search and a full scan, depending on what it can
3820
+ // get away with. The outer hi/lo bounds are only adjusted for
3821
+ // elements that are part of the base order.
3822
+ //
3823
+ // To make sure all elements inside those bounds are visited,
3824
+ // eventually, we keep a set of seen indices, and if the midpoint
3825
+ // has already been handled, we start in a random index within the
3826
+ // current bounds and scan forward until we find an index that
3827
+ // hasn't been seen yet.
3828
+ search: while (lo < hi) {
3829
+ let dist = hi - lo, mid = (lo + hi) >> 1;
3830
+ adjust: if (seen.has(mid)) {
3831
+ let scan = lo + Math.floor(Math.random() * dist);
3832
+ for (let i = 0; i < dist; i++) {
3833
+ if (!seen.has(scan)) {
3834
+ mid = scan;
3835
+ break adjust;
3836
+ }
3837
+ scan++;
3838
+ if (scan == hi)
3839
+ scan = lo; // Wrap around
3812
3840
  }
3841
+ break search; // No index found, we're done
3813
3842
  }
3843
+ seen.add(mid);
3844
+ let rects = getRects(mid);
3845
+ if (rects)
3846
+ for (let i = 0; i < rects.length; i++) {
3847
+ let rect = rects[i], side = 0;
3848
+ if (rect.bottom < this.y) {
3849
+ if (!above || above.bottom < rect.bottom)
3850
+ above = rect;
3851
+ side = 1;
3852
+ }
3853
+ else if (rect.top > this.y) {
3854
+ if (!below || below.top > rect.top)
3855
+ below = rect;
3856
+ side = -1;
3857
+ }
3858
+ else {
3859
+ let off = rect.left > this.x ? this.x - rect.left : rect.right < this.x ? this.x - rect.right : 0;
3860
+ let dx = Math.abs(off);
3861
+ if (dx < closestDx) {
3862
+ closestI = mid;
3863
+ closestDx = dx;
3864
+ closestRect = rect;
3865
+ }
3866
+ if (off)
3867
+ side = (off < 0) == (this.baseDir == exports.Direction.LTR) ? -1 : 1;
3868
+ }
3869
+ // Narrow binary search when it is safe to do so
3870
+ if (side == -1 && (!bidi || this.baseDirAt(positions[mid], 1)))
3871
+ hi = mid;
3872
+ else if (side == 1 && (!bidi || this.baseDirAt(positions[mid + 1], -1)))
3873
+ lo = mid + 1;
3874
+ }
3814
3875
  }
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;
3876
+ // If no element with y overlap is found, find the nearest element
3877
+ // on the y axis, move this.y into it, and retry the scan.
3878
+ if (!closestRect) {
3879
+ let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3880
+ this.y = (side.top + side.bottom) / 2;
3881
+ return this.scan(positions, getRects);
3823
3882
  }
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);
3883
+ let ltr = (bidi ? this.dirAt(positions[closestI], 1) : this.baseDir) == exports.Direction.LTR;
3884
+ return {
3885
+ i: closestI,
3886
+ // Test whether x is closes to the start or end of this element
3887
+ after: (this.x > (closestRect.left + closestRect.right) / 2) == ltr
3888
+ };
3826
3889
  }
3827
- else {
3890
+ scanText(tile, offset) {
3891
+ let positions = [];
3892
+ for (let i = 0; i < tile.length; i = state.findClusterBreak(tile.text, i))
3893
+ positions.push(offset + i);
3894
+ positions.push(offset + tile.length);
3895
+ let scan = this.scan(positions, i => {
3896
+ let off = positions[i] - offset, end = positions[i + 1] - offset;
3897
+ return textRange(tile.dom, off, end).getClientRects();
3898
+ });
3899
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(positions[scan.i], 1);
3900
+ }
3901
+ scanTile(tile, offset) {
3828
3902
  if (!tile.length)
3829
3903
  return new PosAssoc(offset, 1);
3830
- for (let i = 0; i < tile.children.length; i++) {
3904
+ if (tile.children.length == 1) { // Short-circuit single-child tiles
3905
+ let child = tile.children[0];
3906
+ if (child.isText())
3907
+ return this.scanText(child, offset);
3908
+ else if (child.isComposite())
3909
+ return this.scanTile(child, offset);
3910
+ }
3911
+ let positions = [offset];
3912
+ for (let i = 0, pos = offset; i < tile.children.length; i++)
3913
+ positions.push(pos += tile.children[i].length);
3914
+ let scan = this.scan(positions, i => {
3831
3915
  let child = tile.children[i];
3832
3916
  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);
3917
+ return null;
3918
+ return (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3919
+ });
3920
+ let child = tile.children[scan.i], pos = positions[scan.i];
3921
+ if (child.isText())
3922
+ return this.scanText(child, pos);
3923
+ if (child.isComposite())
3924
+ return this.scanTile(child, pos);
3925
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(pos, 1);
3844
3926
  }
3845
3927
  }
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
3928
 
3851
3929
  const LineBreakPlaceholder = "\uffff";
3852
3930
  class DOMReader {
@@ -5000,8 +5078,7 @@ handlers.copy = handlers.cut = (view, event) => {
5000
5078
  // spans multiple elements including this CodeMirror. The copy event may
5001
5079
  // bubble through CodeMirror (e.g. when CodeMirror is the first or the last
5002
5080
  // 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))
5081
+ if (!hasSelection(view.contentDOM, view.observer.selectionRange))
5005
5082
  return false;
5006
5083
  let { text, ranges, linewise } = copiedRange(view.state);
5007
5084
  if (!text && !linewise)
package/dist/index.js CHANGED
@@ -3656,7 +3656,7 @@ function moveVertically(view, start, forward, distance) {
3656
3656
  return EditorSelection.cursor(startPos, start.assoc);
3657
3657
  let goal = start.goalColumn, startY;
3658
3658
  let rect = view.contentDOM.getBoundingClientRect();
3659
- let startCoords = view.coordsAtPos(startPos, start.assoc || -1), docTop = view.documentTop;
3659
+ let startCoords = view.coordsAtPos(startPos, (start.empty ? start.assoc : 0) || (forward ? 1 : -1)), docTop = view.documentTop;
3660
3660
  if (startCoords) {
3661
3661
  if (goal == null)
3662
3662
  goal = startCoords.left - rect.left;
@@ -3740,7 +3740,7 @@ function posAtCoords(view, coords, precise, scanY) {
3740
3740
  if (scanY < 0 ? block.to < view.viewport.from : block.from > view.viewport.to)
3741
3741
  break;
3742
3742
  // Check whether we aren't landing on the top/bottom padding of the line
3743
- let rect = view.docView.coordsAt(scanY < 0 ? block.from : block.to, scanY);
3743
+ let rect = view.docView.coordsAt(scanY < 0 ? block.from : block.to, scanY > 0 ? -1 : 1);
3744
3744
  if (rect && (scanY < 0 ? rect.top <= yOffset + docTop : rect.bottom >= yOffset + docTop))
3745
3745
  break;
3746
3746
  }
@@ -3763,86 +3763,164 @@ function posAtCoords(view, coords, precise, scanY) {
3763
3763
  let line = view.docView.lineAt(block.from, 2);
3764
3764
  if (!line || line.length != block.length)
3765
3765
  line = view.docView.lineAt(block.from, -2);
3766
- return posAtCoordsInline(view, line, block.from, x, y);
3766
+ return new InlineCoordsScan(view, x, y, view.textDirectionAt(block.from)).scanTile(line, block.from);
3767
3767
  }
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;
3768
+ class InlineCoordsScan {
3769
+ constructor(view, x, y, baseDir) {
3770
+ this.view = view;
3771
+ this.x = x;
3772
+ this.y = y;
3773
+ this.baseDir = baseDir;
3774
+ // Cached bidi info
3775
+ this.line = null;
3776
+ this.spans = null;
3777
+ }
3778
+ bidiSpansAt(pos) {
3779
+ if (!this.line || this.line.from > pos || this.line.to < pos) {
3780
+ this.line = this.view.state.doc.lineAt(pos);
3781
+ this.spans = this.view.bidiSpans(this.line);
3782
+ }
3783
+ return this;
3784
+ }
3785
+ baseDirAt(pos, side) {
3786
+ let { line, spans } = this.bidiSpansAt(pos);
3787
+ let level = spans[BidiSpan.find(spans, pos - line.from, -1, side)].level;
3788
+ return level == this.baseDir;
3789
+ }
3790
+ dirAt(pos, side) {
3791
+ let { line, spans } = this.bidiSpansAt(pos);
3792
+ return spans[BidiSpan.find(spans, pos - line.from, -1, side)].dir;
3793
+ }
3794
+ // Used to short-circuit bidi tests for content with a uniform direction
3795
+ bidiIn(from, to) {
3796
+ let { spans, line } = this.bidiSpansAt(from);
3797
+ return spans.length > 1 || spans.length && (spans[0].level != this.baseDir || spans[0].to + line.from < to);
3798
+ }
3799
+ // Scan through the rectangles for the content of a tile with inline
3800
+ // content, looking for one that overlaps the queried position
3801
+ // vertically andis
3802
+ // closest horizontally. The caller is responsible for dividing its
3803
+ // content into N pieces, and pass an array with N+1 positions
3804
+ // (including the position after the last piece). For a text tile,
3805
+ // these will be character clusters, for a composite tile, these
3806
+ // will be child tiles.
3807
+ scan(positions, getRects) {
3808
+ let lo = 0, hi = positions.length - 1, seen = new Set();
3809
+ let bidi = this.bidiIn(positions[0], positions[hi]);
3810
+ let above, below;
3811
+ let closestI = -1, closestDx = 1e9, closestRect;
3812
+ // Because, when the content is bidirectional, a regular binary
3813
+ // search is hard to perform (the content order does not
3814
+ // correspond to visual order), this loop does something between a
3815
+ // regular binary search and a full scan, depending on what it can
3816
+ // get away with. The outer hi/lo bounds are only adjusted for
3817
+ // elements that are part of the base order.
3818
+ //
3819
+ // To make sure all elements inside those bounds are visited,
3820
+ // eventually, we keep a set of seen indices, and if the midpoint
3821
+ // has already been handled, we start in a random index within the
3822
+ // current bounds and scan forward until we find an index that
3823
+ // hasn't been seen yet.
3824
+ search: while (lo < hi) {
3825
+ let dist = hi - lo, mid = (lo + hi) >> 1;
3826
+ adjust: if (seen.has(mid)) {
3827
+ let scan = lo + Math.floor(Math.random() * dist);
3828
+ for (let i = 0; i < dist; i++) {
3829
+ if (!seen.has(scan)) {
3830
+ mid = scan;
3831
+ break adjust;
3832
+ }
3833
+ scan++;
3834
+ if (scan == hi)
3835
+ scan = lo; // Wrap around
3808
3836
  }
3837
+ break search; // No index found, we're done
3809
3838
  }
3839
+ seen.add(mid);
3840
+ let rects = getRects(mid);
3841
+ if (rects)
3842
+ for (let i = 0; i < rects.length; i++) {
3843
+ let rect = rects[i], side = 0;
3844
+ if (rect.bottom < this.y) {
3845
+ if (!above || above.bottom < rect.bottom)
3846
+ above = rect;
3847
+ side = 1;
3848
+ }
3849
+ else if (rect.top > this.y) {
3850
+ if (!below || below.top > rect.top)
3851
+ below = rect;
3852
+ side = -1;
3853
+ }
3854
+ else {
3855
+ let off = rect.left > this.x ? this.x - rect.left : rect.right < this.x ? this.x - rect.right : 0;
3856
+ let dx = Math.abs(off);
3857
+ if (dx < closestDx) {
3858
+ closestI = mid;
3859
+ closestDx = dx;
3860
+ closestRect = rect;
3861
+ }
3862
+ if (off)
3863
+ side = (off < 0) == (this.baseDir == Direction.LTR) ? -1 : 1;
3864
+ }
3865
+ // Narrow binary search when it is safe to do so
3866
+ if (side == -1 && (!bidi || this.baseDirAt(positions[mid], 1)))
3867
+ hi = mid;
3868
+ else if (side == 1 && (!bidi || this.baseDirAt(positions[mid + 1], -1)))
3869
+ lo = mid + 1;
3870
+ }
3810
3871
  }
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;
3872
+ // If no element with y overlap is found, find the nearest element
3873
+ // on the y axis, move this.y into it, and retry the scan.
3874
+ if (!closestRect) {
3875
+ let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3876
+ this.y = (side.top + side.bottom) / 2;
3877
+ return this.scan(positions, getRects);
3819
3878
  }
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);
3879
+ let ltr = (bidi ? this.dirAt(positions[closestI], 1) : this.baseDir) == Direction.LTR;
3880
+ return {
3881
+ i: closestI,
3882
+ // Test whether x is closes to the start or end of this element
3883
+ after: (this.x > (closestRect.left + closestRect.right) / 2) == ltr
3884
+ };
3822
3885
  }
3823
- else {
3886
+ scanText(tile, offset) {
3887
+ let positions = [];
3888
+ for (let i = 0; i < tile.length; i = findClusterBreak(tile.text, i))
3889
+ positions.push(offset + i);
3890
+ positions.push(offset + tile.length);
3891
+ let scan = this.scan(positions, i => {
3892
+ let off = positions[i] - offset, end = positions[i + 1] - offset;
3893
+ return textRange(tile.dom, off, end).getClientRects();
3894
+ });
3895
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(positions[scan.i], 1);
3896
+ }
3897
+ scanTile(tile, offset) {
3824
3898
  if (!tile.length)
3825
3899
  return new PosAssoc(offset, 1);
3826
- for (let i = 0; i < tile.children.length; i++) {
3900
+ if (tile.children.length == 1) { // Short-circuit single-child tiles
3901
+ let child = tile.children[0];
3902
+ if (child.isText())
3903
+ return this.scanText(child, offset);
3904
+ else if (child.isComposite())
3905
+ return this.scanTile(child, offset);
3906
+ }
3907
+ let positions = [offset];
3908
+ for (let i = 0, pos = offset; i < tile.children.length; i++)
3909
+ positions.push(pos += tile.children[i].length);
3910
+ let scan = this.scan(positions, i => {
3827
3911
  let child = tile.children[i];
3828
3912
  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);
3913
+ return null;
3914
+ return (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3915
+ });
3916
+ let child = tile.children[scan.i], pos = positions[scan.i];
3917
+ if (child.isText())
3918
+ return this.scanText(child, pos);
3919
+ if (child.isComposite())
3920
+ return this.scanTile(child, pos);
3921
+ return scan.after ? new PosAssoc(positions[scan.i + 1], -1) : new PosAssoc(pos, 1);
3840
3922
  }
3841
3923
  }
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
3924
 
3847
3925
  const LineBreakPlaceholder = "\uffff";
3848
3926
  class DOMReader {
@@ -4996,8 +5074,7 @@ handlers.copy = handlers.cut = (view, event) => {
4996
5074
  // spans multiple elements including this CodeMirror. The copy event may
4997
5075
  // bubble through CodeMirror (e.g. when CodeMirror is the first or the last
4998
5076
  // 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))
5077
+ if (!hasSelection(view.contentDOM, view.observer.selectionRange))
5001
5078
  return false;
5002
5079
  let { text, ranges, linewise } = copiedRange(view.state);
5003
5080
  if (!text && !linewise)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.12",
3
+ "version": "6.39.14",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",