@codemirror/view 6.38.2 → 6.38.4

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.38.4 (2025-09-28)
2
+
3
+ ### Bug fixes
4
+
5
+ Work around a Chrome Android issue where the browser doesn't properly fire composition end events, leaving CodeMirror to believe the user was still composing.
6
+
7
+ ## 6.38.3 (2025-09-22)
8
+
9
+ ### Bug fixes
10
+
11
+ Work around a rendering bug in Mobile Safari by completely hiding empty layers.
12
+
13
+ Fix vertical cursor motion in Chrome around decorations with bottom borders or margins.
14
+
15
+ Fix an issue that caused mark decorations longer than 512 characters to needlessly be split.
16
+
17
+ Move the cursor out of atomic ranges when text input happens.
18
+
1
19
  ## 6.38.2 (2025-09-01)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -1786,13 +1786,14 @@ class ContentBuilder {
1786
1786
  this.textOff = 0;
1787
1787
  }
1788
1788
  }
1789
- let take = Math.min(this.text.length - this.textOff, length, 512 /* T.Chunk */);
1789
+ let remaining = Math.min(this.text.length - this.textOff, length);
1790
+ let take = Math.min(remaining, 512 /* T.Chunk */);
1790
1791
  this.flushBuffer(active.slice(active.length - openStart));
1791
1792
  this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1792
1793
  this.atCursorPos = true;
1793
1794
  this.textOff += take;
1794
1795
  length -= take;
1795
- openStart = 0;
1796
+ openStart = remaining <= take ? 0 : active.length;
1796
1797
  }
1797
1798
  }
1798
1799
  span(from, to, active, openStart) {
@@ -3607,14 +3608,13 @@ function posAtCoords(view, coords, precise, bias = -1) {
3607
3608
  }
3608
3609
  else if (doc.caretRangeFromPoint) {
3609
3610
  let range = doc.caretRangeFromPoint(x, y);
3610
- if (range) {
3611
+ if (range)
3611
3612
  ({ startContainer: node, startOffset: offset } = range);
3612
- if (!view.contentDOM.contains(node) ||
3613
- browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3614
- browser.chrome && isSuspiciousChromeCaretResult(node, offset, x))
3615
- node = undefined;
3616
- }
3617
3613
  }
3614
+ if (node && (!view.contentDOM.contains(node) ||
3615
+ browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3616
+ browser.chrome && isSuspiciousChromeCaretResult(node, offset, x)))
3617
+ node = undefined;
3618
3618
  // Chrome will return offsets into <input> elements without child
3619
3619
  // nodes, which will lead to a null deref below, so clip the
3620
3620
  // offset to the node size.
@@ -3650,11 +3650,7 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3650
3650
  let content = view.state.sliceDoc(block.from, block.to);
3651
3651
  return block.from + state.findColumn(content, into, view.state.tabSize);
3652
3652
  }
3653
- // In case of a high line height, Safari's caretRangeFromPoint treats
3654
- // the space between lines as belonging to the last character of the
3655
- // line before. This is used to detect such a result so that it can be
3656
- // ignored (issue #401).
3657
- function isSuspiciousSafariCaretResult(node, offset, x) {
3653
+ function isEndOfLineBefore(node, offset, x) {
3658
3654
  let len, scan = node;
3659
3655
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3660
3656
  return false;
@@ -3674,10 +3670,17 @@ function isSuspiciousSafariCaretResult(node, offset, x) {
3674
3670
  }
3675
3671
  return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3676
3672
  }
3673
+ // In case of a high line height, Safari's caretRangeFromPoint treats
3674
+ // the space between lines as belonging to the last character of the
3675
+ // line before. This is used to detect such a result so that it can be
3676
+ // ignored (issue #401).
3677
+ function isSuspiciousSafariCaretResult(node, offset, x) {
3678
+ return isEndOfLineBefore(node, offset, x);
3679
+ }
3677
3680
  // Chrome will move positions between lines to the start of the next line
3678
3681
  function isSuspiciousChromeCaretResult(node, offset, x) {
3679
3682
  if (offset != 0)
3680
- return false;
3683
+ return isEndOfLineBefore(node, offset, x);
3681
3684
  for (let cur = node;;) {
3682
3685
  let parent = cur.parentNode;
3683
3686
  if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
@@ -4103,8 +4106,20 @@ function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
4103
4106
  return true;
4104
4107
  }
4105
4108
  function applyDefaultInsert(view, change, newSel) {
4106
- let tr, startState = view.state, sel = startState.selection.main;
4107
- if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
4109
+ let tr, startState = view.state, sel = startState.selection.main, inAtomic = -1;
4110
+ if (change.from == change.to && change.from < sel.from || change.from > sel.to) {
4111
+ let side = change.from < sel.from ? -1 : 1, pos = side < 0 ? sel.from : sel.to;
4112
+ let moved = skipAtomicRanges(startState.facet(atomicRanges).map(f => f(view)), pos, side);
4113
+ if (change.from == moved)
4114
+ inAtomic = moved;
4115
+ }
4116
+ if (inAtomic > -1) {
4117
+ tr = {
4118
+ changes: change,
4119
+ selection: state.EditorSelection.cursor(change.from + change.insert.length, -1)
4120
+ };
4121
+ }
4122
+ else if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
4108
4123
  (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
4109
4124
  view.inputState.composing < 0) {
4110
4125
  let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
@@ -7298,6 +7313,10 @@ class EditContextManager {
7298
7313
  this.revertPending(view.state);
7299
7314
  this.setSelection(view.state);
7300
7315
  }
7316
+ // Work around missed compositionend events. See https://discuss.codemirror.net/t/a/9514
7317
+ if (change.from < change.to && !change.insert.length && view.inputState.composing >= 0 &&
7318
+ !/[\\p{Alphabetic}\\p{Number}_]/.test(context.text.slice(Math.max(0, e.updateRangeStart - 1), Math.min(context.text.length, e.updateRangeStart + 1))))
7319
+ this.handlers.compositionend(e);
7301
7320
  };
7302
7321
  this.handlers.characterboundsupdate = e => {
7303
7322
  let rects = [], prev = null;
@@ -7313,10 +7332,11 @@ class EditContextManager {
7313
7332
  let deco = [];
7314
7333
  for (let format of e.getTextFormats()) {
7315
7334
  let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
7316
- if (lineStyle != "None" && thickness != "None") {
7335
+ if (!/none/i.test(lineStyle) && !/none/i.test(thickness)) {
7317
7336
  let from = this.toEditorPos(format.rangeStart), to = this.toEditorPos(format.rangeEnd);
7318
7337
  if (from < to) {
7319
- let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
7338
+ // These values changed from capitalized custom strings to lower-case CSS keywords in 2025
7339
+ let style = `text-decoration: underline ${/^[a-z]/.test(lineStyle) ? lineStyle + " " : lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${/thin/i.test(thickness) ? 1 : 2}px`;
7320
7340
  deco.push(Decoration.mark({ attributes: { style } }).range(from, to));
7321
7341
  }
7322
7342
  }
@@ -9083,6 +9103,8 @@ class LayerView {
9083
9103
  old = next;
9084
9104
  }
9085
9105
  this.drawn = markers;
9106
+ if (browser.ios) // Issue #1600
9107
+ this.dom.style.display = this.dom.firstChild ? "" : "none";
9086
9108
  }
9087
9109
  }
9088
9110
  destroy() {
package/dist/index.js CHANGED
@@ -1783,13 +1783,14 @@ class ContentBuilder {
1783
1783
  this.textOff = 0;
1784
1784
  }
1785
1785
  }
1786
- let take = Math.min(this.text.length - this.textOff, length, 512 /* T.Chunk */);
1786
+ let remaining = Math.min(this.text.length - this.textOff, length);
1787
+ let take = Math.min(remaining, 512 /* T.Chunk */);
1787
1788
  this.flushBuffer(active.slice(active.length - openStart));
1788
1789
  this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1789
1790
  this.atCursorPos = true;
1790
1791
  this.textOff += take;
1791
1792
  length -= take;
1792
- openStart = 0;
1793
+ openStart = remaining <= take ? 0 : active.length;
1793
1794
  }
1794
1795
  }
1795
1796
  span(from, to, active, openStart) {
@@ -3603,14 +3604,13 @@ function posAtCoords(view, coords, precise, bias = -1) {
3603
3604
  }
3604
3605
  else if (doc.caretRangeFromPoint) {
3605
3606
  let range = doc.caretRangeFromPoint(x, y);
3606
- if (range) {
3607
+ if (range)
3607
3608
  ({ startContainer: node, startOffset: offset } = range);
3608
- if (!view.contentDOM.contains(node) ||
3609
- browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3610
- browser.chrome && isSuspiciousChromeCaretResult(node, offset, x))
3611
- node = undefined;
3612
- }
3613
3609
  }
3610
+ if (node && (!view.contentDOM.contains(node) ||
3611
+ browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3612
+ browser.chrome && isSuspiciousChromeCaretResult(node, offset, x)))
3613
+ node = undefined;
3614
3614
  // Chrome will return offsets into <input> elements without child
3615
3615
  // nodes, which will lead to a null deref below, so clip the
3616
3616
  // offset to the node size.
@@ -3646,11 +3646,7 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3646
3646
  let content = view.state.sliceDoc(block.from, block.to);
3647
3647
  return block.from + findColumn(content, into, view.state.tabSize);
3648
3648
  }
3649
- // In case of a high line height, Safari's caretRangeFromPoint treats
3650
- // the space between lines as belonging to the last character of the
3651
- // line before. This is used to detect such a result so that it can be
3652
- // ignored (issue #401).
3653
- function isSuspiciousSafariCaretResult(node, offset, x) {
3649
+ function isEndOfLineBefore(node, offset, x) {
3654
3650
  let len, scan = node;
3655
3651
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3656
3652
  return false;
@@ -3670,10 +3666,17 @@ function isSuspiciousSafariCaretResult(node, offset, x) {
3670
3666
  }
3671
3667
  return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3672
3668
  }
3669
+ // In case of a high line height, Safari's caretRangeFromPoint treats
3670
+ // the space between lines as belonging to the last character of the
3671
+ // line before. This is used to detect such a result so that it can be
3672
+ // ignored (issue #401).
3673
+ function isSuspiciousSafariCaretResult(node, offset, x) {
3674
+ return isEndOfLineBefore(node, offset, x);
3675
+ }
3673
3676
  // Chrome will move positions between lines to the start of the next line
3674
3677
  function isSuspiciousChromeCaretResult(node, offset, x) {
3675
3678
  if (offset != 0)
3676
- return false;
3679
+ return isEndOfLineBefore(node, offset, x);
3677
3680
  for (let cur = node;;) {
3678
3681
  let parent = cur.parentNode;
3679
3682
  if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
@@ -4099,8 +4102,20 @@ function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
4099
4102
  return true;
4100
4103
  }
4101
4104
  function applyDefaultInsert(view, change, newSel) {
4102
- let tr, startState = view.state, sel = startState.selection.main;
4103
- if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
4105
+ let tr, startState = view.state, sel = startState.selection.main, inAtomic = -1;
4106
+ if (change.from == change.to && change.from < sel.from || change.from > sel.to) {
4107
+ let side = change.from < sel.from ? -1 : 1, pos = side < 0 ? sel.from : sel.to;
4108
+ let moved = skipAtomicRanges(startState.facet(atomicRanges).map(f => f(view)), pos, side);
4109
+ if (change.from == moved)
4110
+ inAtomic = moved;
4111
+ }
4112
+ if (inAtomic > -1) {
4113
+ tr = {
4114
+ changes: change,
4115
+ selection: EditorSelection.cursor(change.from + change.insert.length, -1)
4116
+ };
4117
+ }
4118
+ else if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
4104
4119
  (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
4105
4120
  view.inputState.composing < 0) {
4106
4121
  let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
@@ -7293,6 +7308,10 @@ class EditContextManager {
7293
7308
  this.revertPending(view.state);
7294
7309
  this.setSelection(view.state);
7295
7310
  }
7311
+ // Work around missed compositionend events. See https://discuss.codemirror.net/t/a/9514
7312
+ if (change.from < change.to && !change.insert.length && view.inputState.composing >= 0 &&
7313
+ !/[\\p{Alphabetic}\\p{Number}_]/.test(context.text.slice(Math.max(0, e.updateRangeStart - 1), Math.min(context.text.length, e.updateRangeStart + 1))))
7314
+ this.handlers.compositionend(e);
7296
7315
  };
7297
7316
  this.handlers.characterboundsupdate = e => {
7298
7317
  let rects = [], prev = null;
@@ -7308,10 +7327,11 @@ class EditContextManager {
7308
7327
  let deco = [];
7309
7328
  for (let format of e.getTextFormats()) {
7310
7329
  let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
7311
- if (lineStyle != "None" && thickness != "None") {
7330
+ if (!/none/i.test(lineStyle) && !/none/i.test(thickness)) {
7312
7331
  let from = this.toEditorPos(format.rangeStart), to = this.toEditorPos(format.rangeEnd);
7313
7332
  if (from < to) {
7314
- let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
7333
+ // These values changed from capitalized custom strings to lower-case CSS keywords in 2025
7334
+ let style = `text-decoration: underline ${/^[a-z]/.test(lineStyle) ? lineStyle + " " : lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${/thin/i.test(thickness) ? 1 : 2}px`;
7315
7335
  deco.push(Decoration.mark({ attributes: { style } }).range(from, to));
7316
7336
  }
7317
7337
  }
@@ -9078,6 +9098,8 @@ class LayerView {
9078
9098
  old = next;
9079
9099
  }
9080
9100
  this.drawn = markers;
9101
+ if (browser.ios) // Issue #1600
9102
+ this.dom.style.display = this.dom.firstChild ? "" : "none";
9081
9103
  }
9082
9104
  }
9083
9105
  destroy() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.38.2",
3
+ "version": "6.38.4",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",