@codemirror/view 6.37.1 → 6.38.0

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,17 @@
1
+ ## 6.38.0 (2025-06-27)
2
+
3
+ ### New features
4
+
5
+ Gutters can now specify that they should be displayed after the content (which would be to the right in a left-to-right layout).
6
+
7
+ ## 6.37.2 (2025-06-12)
8
+
9
+ ### Bug fixes
10
+
11
+ Fix an issue where moving the cursor vertically from the one-but-last character on a line would sometimes move incorrectly on Safari.
12
+
13
+ Fix an issue causing coordinates between lines of text to sometimes be inappropriately placed at the end of the line by `posAtCoords`.
14
+
1
15
  ## 6.37.1 (2025-05-30)
2
16
 
3
17
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -3479,8 +3479,7 @@ function domPosAtCoords(parent, x, y) {
3479
3479
  closestRect = rect;
3480
3480
  closestX = dx;
3481
3481
  closestY = dy;
3482
- let side = dy ? (y < rect.top ? -1 : 1) : dx ? (x < rect.left ? -1 : 1) : 0;
3483
- closestOverlap = !side || (side > 0 ? i < rects.length - 1 : i > 0);
3482
+ closestOverlap = !dx ? true : x < rect.left ? i > 0 : i < rects.length - 1;
3484
3483
  }
3485
3484
  if (dx == 0) {
3486
3485
  if (y > rect.bottom && (!aboveRect || aboveRect.bottom < rect.bottom)) {
@@ -3656,13 +3655,24 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3656
3655
  // line before. This is used to detect such a result so that it can be
3657
3656
  // ignored (issue #401).
3658
3657
  function isSuspiciousSafariCaretResult(node, offset, x) {
3659
- let len;
3658
+ let len, scan = node;
3660
3659
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3661
3660
  return false;
3662
- for (let next = node.nextSibling; next; next = next.nextSibling)
3663
- if (next.nodeType != 1 || next.nodeName != "BR")
3661
+ for (;;) { // Check that there is no content after this node
3662
+ let next = scan.nextSibling;
3663
+ if (next) {
3664
+ if (next.nodeName == "BR")
3665
+ break;
3664
3666
  return false;
3665
- return textRange(node, len - 1, len).getBoundingClientRect().left > x;
3667
+ }
3668
+ else {
3669
+ let parent = scan.parentNode;
3670
+ if (!parent || parent.nodeName == "DIV")
3671
+ break;
3672
+ scan = parent;
3673
+ }
3674
+ }
3675
+ return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3666
3676
  }
3667
3677
  // Chrome will move positions between lines to the start of the next line
3668
3678
  function isSuspiciousChromeCaretResult(node, offset, x) {
@@ -6581,13 +6591,16 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6581
6591
  display: "flex",
6582
6592
  height: "100%",
6583
6593
  boxSizing: "border-box",
6584
- insetInlineStart: 0,
6585
- zIndex: 200
6594
+ zIndex: 200,
6586
6595
  },
6596
+ ".cm-gutters-before": { insetInlineStart: 0 },
6597
+ ".cm-gutters-after": { insetInlineEnd: 0 },
6587
6598
  "&light .cm-gutters": {
6588
6599
  backgroundColor: "#f5f5f5",
6589
6600
  color: "#6c6c6c",
6590
- borderRight: "1px solid #ddd"
6601
+ border: "0px solid #ddd",
6602
+ "&.cm-gutters-before": { borderRightWidth: "1px" },
6603
+ "&.cm-gutters-after": { borderLeftWidth: "1px" },
6591
6604
  },
6592
6605
  "&dark .cm-gutters": {
6593
6606
  backgroundColor: "#333338",
@@ -6778,7 +6791,7 @@ class DOMObserver {
6778
6791
  else
6779
6792
  this.flush();
6780
6793
  });
6781
- if (window.EditContext && view.constructor.EDIT_CONTEXT !== false &&
6794
+ if (window.EditContext && browser.android && view.constructor.EDIT_CONTEXT !== false &&
6782
6795
  // Chrome <126 doesn't support inverted selections in edit context (#1392)
6783
6796
  !(browser.chrome && browser.chrome_version < 126)) {
6784
6797
  this.editContext = new EditContextManager(view);
@@ -10858,7 +10871,8 @@ const defaults = {
10858
10871
  lineMarkerChange: null,
10859
10872
  initialSpacer: null,
10860
10873
  updateSpacer: null,
10861
- domEventHandlers: {}
10874
+ domEventHandlers: {},
10875
+ side: "before"
10862
10876
  };
10863
10877
  const activeGutters = state.Facet.define();
10864
10878
  /**
@@ -10892,15 +10906,20 @@ function gutters(config) {
10892
10906
  const gutterView = ViewPlugin.fromClass(class {
10893
10907
  constructor(view) {
10894
10908
  this.view = view;
10909
+ this.domAfter = null;
10895
10910
  this.prevViewport = view.viewport;
10896
10911
  this.dom = document.createElement("div");
10897
- this.dom.className = "cm-gutters";
10912
+ this.dom.className = "cm-gutters cm-gutters-before";
10898
10913
  this.dom.setAttribute("aria-hidden", "true");
10899
10914
  this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10900
10915
  this.gutters = view.state.facet(activeGutters).map(conf => new SingleGutterView(view, conf));
10901
- for (let gutter of this.gutters)
10902
- this.dom.appendChild(gutter.dom);
10903
10916
  this.fixed = !view.state.facet(unfixGutters);
10917
+ for (let gutter of this.gutters) {
10918
+ if (gutter.config.side == "after")
10919
+ this.getDOMAfter().appendChild(gutter.dom);
10920
+ else
10921
+ this.dom.appendChild(gutter.dom);
10922
+ }
10904
10923
  if (this.fixed) {
10905
10924
  // FIXME IE11 fallback, which doesn't support position: sticky,
10906
10925
  // by using position: relative + event handlers that realign the
@@ -10910,6 +10929,17 @@ const gutterView = ViewPlugin.fromClass(class {
10910
10929
  this.syncGutters(false);
10911
10930
  view.scrollDOM.insertBefore(this.dom, view.contentDOM);
10912
10931
  }
10932
+ getDOMAfter() {
10933
+ if (!this.domAfter) {
10934
+ this.domAfter = document.createElement("div");
10935
+ this.domAfter.className = "cm-gutters cm-gutters-after";
10936
+ this.domAfter.setAttribute("aria-hidden", "true");
10937
+ this.domAfter.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10938
+ this.domAfter.style.position = this.fixed ? "sticky" : "";
10939
+ this.view.scrollDOM.appendChild(this.domAfter);
10940
+ }
10941
+ return this.domAfter;
10942
+ }
10913
10943
  update(update) {
10914
10944
  if (this.updateGutters(update)) {
10915
10945
  // Detach during sync when the viewport changed significantly
@@ -10920,18 +10950,26 @@ const gutterView = ViewPlugin.fromClass(class {
10920
10950
  this.syncGutters(vpOverlap < (vpB.to - vpB.from) * 0.8);
10921
10951
  }
10922
10952
  if (update.geometryChanged) {
10923
- this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10953
+ let min = (this.view.contentHeight / this.view.scaleY) + "px";
10954
+ this.dom.style.minHeight = min;
10955
+ if (this.domAfter)
10956
+ this.domAfter.style.minHeight = min;
10924
10957
  }
10925
10958
  if (this.view.state.facet(unfixGutters) != !this.fixed) {
10926
10959
  this.fixed = !this.fixed;
10927
10960
  this.dom.style.position = this.fixed ? "sticky" : "";
10961
+ if (this.domAfter)
10962
+ this.domAfter.style.position = this.fixed ? "sticky" : "";
10928
10963
  }
10929
10964
  this.prevViewport = update.view.viewport;
10930
10965
  }
10931
10966
  syncGutters(detach) {
10932
10967
  let after = this.dom.nextSibling;
10933
- if (detach)
10968
+ if (detach) {
10934
10969
  this.dom.remove();
10970
+ if (this.domAfter)
10971
+ this.domAfter.remove();
10972
+ }
10935
10973
  let lineClasses = state.RangeSet.iter(this.view.state.facet(gutterLineClass), this.view.viewport.from);
10936
10974
  let classSet = [];
10937
10975
  let contexts = this.gutters.map(gutter => new UpdateContext(gutter, this.view.viewport, -this.view.documentPadding.top));
@@ -10965,8 +11003,11 @@ const gutterView = ViewPlugin.fromClass(class {
10965
11003
  }
10966
11004
  for (let cx of contexts)
10967
11005
  cx.finish();
10968
- if (detach)
11006
+ if (detach) {
10969
11007
  this.view.scrollDOM.insertBefore(this.dom, after);
11008
+ if (this.domAfter)
11009
+ this.view.scrollDOM.appendChild(this.domAfter);
11010
+ }
10970
11011
  }
10971
11012
  updateGutters(update) {
10972
11013
  let prev = update.startState.facet(activeGutters), cur = update.state.facet(activeGutters);
@@ -10995,8 +11036,12 @@ const gutterView = ViewPlugin.fromClass(class {
10995
11036
  if (gutters.indexOf(g) < 0)
10996
11037
  g.destroy();
10997
11038
  }
10998
- for (let g of gutters)
10999
- this.dom.appendChild(g.dom);
11039
+ for (let g of gutters) {
11040
+ if (g.config.side == "after")
11041
+ this.getDOMAfter().appendChild(g.dom);
11042
+ else
11043
+ this.dom.appendChild(g.dom);
11044
+ }
11000
11045
  this.gutters = gutters;
11001
11046
  }
11002
11047
  return change;
@@ -11005,15 +11050,18 @@ const gutterView = ViewPlugin.fromClass(class {
11005
11050
  for (let view of this.gutters)
11006
11051
  view.destroy();
11007
11052
  this.dom.remove();
11053
+ if (this.domAfter)
11054
+ this.domAfter.remove();
11008
11055
  }
11009
11056
  }, {
11010
11057
  provide: plugin => EditorView.scrollMargins.of(view => {
11011
11058
  let value = view.plugin(plugin);
11012
11059
  if (!value || value.gutters.length == 0 || !value.fixed)
11013
11060
  return null;
11061
+ let before = value.dom.offsetWidth * view.scaleX, after = value.domAfter ? value.domAfter.offsetWidth * view.scaleX : 0;
11014
11062
  return view.textDirection == exports.Direction.LTR
11015
- ? { left: value.dom.offsetWidth * view.scaleX }
11016
- : { right: value.dom.offsetWidth * view.scaleX };
11063
+ ? { left: before, right: after }
11064
+ : { right: before, left: after };
11017
11065
  })
11018
11066
  });
11019
11067
  function asArray(val) { return (Array.isArray(val) ? val : [val]); }
@@ -11255,7 +11303,8 @@ const lineNumberGutter = activeGutters.compute([lineNumberConfig], state => ({
11255
11303
  let max = formatNumber(update.view, maxLineNumber(update.view.state.doc.lines));
11256
11304
  return max == spacer.number ? spacer : new NumberMarker(max);
11257
11305
  },
11258
- domEventHandlers: state.facet(lineNumberConfig).domEventHandlers
11306
+ domEventHandlers: state.facet(lineNumberConfig).domEventHandlers,
11307
+ side: "before"
11259
11308
  }));
11260
11309
  /**
11261
11310
  Create a line number gutter extension.
package/dist/index.d.cts CHANGED
@@ -2235,6 +2235,12 @@ interface GutterConfig {
2235
2235
  Supply event handlers for DOM events on this gutter.
2236
2236
  */
2237
2237
  domEventHandlers?: Handlers;
2238
+ /**
2239
+ By default, gutters are shown horizontally before the editor
2240
+ content (to the left in a left-to-right layout). Set this to
2241
+ `"after"` to show a gutter on the other side of the content.
2242
+ */
2243
+ side?: "before" | "after";
2238
2244
  }
2239
2245
  /**
2240
2246
  Define an editor gutter. The order in which the gutters appear is
package/dist/index.d.ts CHANGED
@@ -2235,6 +2235,12 @@ interface GutterConfig {
2235
2235
  Supply event handlers for DOM events on this gutter.
2236
2236
  */
2237
2237
  domEventHandlers?: Handlers;
2238
+ /**
2239
+ By default, gutters are shown horizontally before the editor
2240
+ content (to the left in a left-to-right layout). Set this to
2241
+ `"after"` to show a gutter on the other side of the content.
2242
+ */
2243
+ side?: "before" | "after";
2238
2244
  }
2239
2245
  /**
2240
2246
  Define an editor gutter. The order in which the gutters appear is
package/dist/index.js CHANGED
@@ -3475,8 +3475,7 @@ function domPosAtCoords(parent, x, y) {
3475
3475
  closestRect = rect;
3476
3476
  closestX = dx;
3477
3477
  closestY = dy;
3478
- let side = dy ? (y < rect.top ? -1 : 1) : dx ? (x < rect.left ? -1 : 1) : 0;
3479
- closestOverlap = !side || (side > 0 ? i < rects.length - 1 : i > 0);
3478
+ closestOverlap = !dx ? true : x < rect.left ? i > 0 : i < rects.length - 1;
3480
3479
  }
3481
3480
  if (dx == 0) {
3482
3481
  if (y > rect.bottom && (!aboveRect || aboveRect.bottom < rect.bottom)) {
@@ -3652,13 +3651,24 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3652
3651
  // line before. This is used to detect such a result so that it can be
3653
3652
  // ignored (issue #401).
3654
3653
  function isSuspiciousSafariCaretResult(node, offset, x) {
3655
- let len;
3654
+ let len, scan = node;
3656
3655
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3657
3656
  return false;
3658
- for (let next = node.nextSibling; next; next = next.nextSibling)
3659
- if (next.nodeType != 1 || next.nodeName != "BR")
3657
+ for (;;) { // Check that there is no content after this node
3658
+ let next = scan.nextSibling;
3659
+ if (next) {
3660
+ if (next.nodeName == "BR")
3661
+ break;
3660
3662
  return false;
3661
- return textRange(node, len - 1, len).getBoundingClientRect().left > x;
3663
+ }
3664
+ else {
3665
+ let parent = scan.parentNode;
3666
+ if (!parent || parent.nodeName == "DIV")
3667
+ break;
3668
+ scan = parent;
3669
+ }
3670
+ }
3671
+ return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3662
3672
  }
3663
3673
  // Chrome will move positions between lines to the start of the next line
3664
3674
  function isSuspiciousChromeCaretResult(node, offset, x) {
@@ -6576,13 +6586,16 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6576
6586
  display: "flex",
6577
6587
  height: "100%",
6578
6588
  boxSizing: "border-box",
6579
- insetInlineStart: 0,
6580
- zIndex: 200
6589
+ zIndex: 200,
6581
6590
  },
6591
+ ".cm-gutters-before": { insetInlineStart: 0 },
6592
+ ".cm-gutters-after": { insetInlineEnd: 0 },
6582
6593
  "&light .cm-gutters": {
6583
6594
  backgroundColor: "#f5f5f5",
6584
6595
  color: "#6c6c6c",
6585
- borderRight: "1px solid #ddd"
6596
+ border: "0px solid #ddd",
6597
+ "&.cm-gutters-before": { borderRightWidth: "1px" },
6598
+ "&.cm-gutters-after": { borderLeftWidth: "1px" },
6586
6599
  },
6587
6600
  "&dark .cm-gutters": {
6588
6601
  backgroundColor: "#333338",
@@ -6773,7 +6786,7 @@ class DOMObserver {
6773
6786
  else
6774
6787
  this.flush();
6775
6788
  });
6776
- if (window.EditContext && view.constructor.EDIT_CONTEXT !== false &&
6789
+ if (window.EditContext && browser.android && view.constructor.EDIT_CONTEXT !== false &&
6777
6790
  // Chrome <126 doesn't support inverted selections in edit context (#1392)
6778
6791
  !(browser.chrome && browser.chrome_version < 126)) {
6779
6792
  this.editContext = new EditContextManager(view);
@@ -10853,7 +10866,8 @@ const defaults = {
10853
10866
  lineMarkerChange: null,
10854
10867
  initialSpacer: null,
10855
10868
  updateSpacer: null,
10856
- domEventHandlers: {}
10869
+ domEventHandlers: {},
10870
+ side: "before"
10857
10871
  };
10858
10872
  const activeGutters = /*@__PURE__*/Facet.define();
10859
10873
  /**
@@ -10887,15 +10901,20 @@ function gutters(config) {
10887
10901
  const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
10888
10902
  constructor(view) {
10889
10903
  this.view = view;
10904
+ this.domAfter = null;
10890
10905
  this.prevViewport = view.viewport;
10891
10906
  this.dom = document.createElement("div");
10892
- this.dom.className = "cm-gutters";
10907
+ this.dom.className = "cm-gutters cm-gutters-before";
10893
10908
  this.dom.setAttribute("aria-hidden", "true");
10894
10909
  this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10895
10910
  this.gutters = view.state.facet(activeGutters).map(conf => new SingleGutterView(view, conf));
10896
- for (let gutter of this.gutters)
10897
- this.dom.appendChild(gutter.dom);
10898
10911
  this.fixed = !view.state.facet(unfixGutters);
10912
+ for (let gutter of this.gutters) {
10913
+ if (gutter.config.side == "after")
10914
+ this.getDOMAfter().appendChild(gutter.dom);
10915
+ else
10916
+ this.dom.appendChild(gutter.dom);
10917
+ }
10899
10918
  if (this.fixed) {
10900
10919
  // FIXME IE11 fallback, which doesn't support position: sticky,
10901
10920
  // by using position: relative + event handlers that realign the
@@ -10905,6 +10924,17 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
10905
10924
  this.syncGutters(false);
10906
10925
  view.scrollDOM.insertBefore(this.dom, view.contentDOM);
10907
10926
  }
10927
+ getDOMAfter() {
10928
+ if (!this.domAfter) {
10929
+ this.domAfter = document.createElement("div");
10930
+ this.domAfter.className = "cm-gutters cm-gutters-after";
10931
+ this.domAfter.setAttribute("aria-hidden", "true");
10932
+ this.domAfter.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10933
+ this.domAfter.style.position = this.fixed ? "sticky" : "";
10934
+ this.view.scrollDOM.appendChild(this.domAfter);
10935
+ }
10936
+ return this.domAfter;
10937
+ }
10908
10938
  update(update) {
10909
10939
  if (this.updateGutters(update)) {
10910
10940
  // Detach during sync when the viewport changed significantly
@@ -10915,18 +10945,26 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
10915
10945
  this.syncGutters(vpOverlap < (vpB.to - vpB.from) * 0.8);
10916
10946
  }
10917
10947
  if (update.geometryChanged) {
10918
- this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
10948
+ let min = (this.view.contentHeight / this.view.scaleY) + "px";
10949
+ this.dom.style.minHeight = min;
10950
+ if (this.domAfter)
10951
+ this.domAfter.style.minHeight = min;
10919
10952
  }
10920
10953
  if (this.view.state.facet(unfixGutters) != !this.fixed) {
10921
10954
  this.fixed = !this.fixed;
10922
10955
  this.dom.style.position = this.fixed ? "sticky" : "";
10956
+ if (this.domAfter)
10957
+ this.domAfter.style.position = this.fixed ? "sticky" : "";
10923
10958
  }
10924
10959
  this.prevViewport = update.view.viewport;
10925
10960
  }
10926
10961
  syncGutters(detach) {
10927
10962
  let after = this.dom.nextSibling;
10928
- if (detach)
10963
+ if (detach) {
10929
10964
  this.dom.remove();
10965
+ if (this.domAfter)
10966
+ this.domAfter.remove();
10967
+ }
10930
10968
  let lineClasses = RangeSet.iter(this.view.state.facet(gutterLineClass), this.view.viewport.from);
10931
10969
  let classSet = [];
10932
10970
  let contexts = this.gutters.map(gutter => new UpdateContext(gutter, this.view.viewport, -this.view.documentPadding.top));
@@ -10960,8 +10998,11 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
10960
10998
  }
10961
10999
  for (let cx of contexts)
10962
11000
  cx.finish();
10963
- if (detach)
11001
+ if (detach) {
10964
11002
  this.view.scrollDOM.insertBefore(this.dom, after);
11003
+ if (this.domAfter)
11004
+ this.view.scrollDOM.appendChild(this.domAfter);
11005
+ }
10965
11006
  }
10966
11007
  updateGutters(update) {
10967
11008
  let prev = update.startState.facet(activeGutters), cur = update.state.facet(activeGutters);
@@ -10990,8 +11031,12 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
10990
11031
  if (gutters.indexOf(g) < 0)
10991
11032
  g.destroy();
10992
11033
  }
10993
- for (let g of gutters)
10994
- this.dom.appendChild(g.dom);
11034
+ for (let g of gutters) {
11035
+ if (g.config.side == "after")
11036
+ this.getDOMAfter().appendChild(g.dom);
11037
+ else
11038
+ this.dom.appendChild(g.dom);
11039
+ }
10995
11040
  this.gutters = gutters;
10996
11041
  }
10997
11042
  return change;
@@ -11000,15 +11045,18 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
11000
11045
  for (let view of this.gutters)
11001
11046
  view.destroy();
11002
11047
  this.dom.remove();
11048
+ if (this.domAfter)
11049
+ this.domAfter.remove();
11003
11050
  }
11004
11051
  }, {
11005
11052
  provide: plugin => EditorView.scrollMargins.of(view => {
11006
11053
  let value = view.plugin(plugin);
11007
11054
  if (!value || value.gutters.length == 0 || !value.fixed)
11008
11055
  return null;
11056
+ let before = value.dom.offsetWidth * view.scaleX, after = value.domAfter ? value.domAfter.offsetWidth * view.scaleX : 0;
11009
11057
  return view.textDirection == Direction.LTR
11010
- ? { left: value.dom.offsetWidth * view.scaleX }
11011
- : { right: value.dom.offsetWidth * view.scaleX };
11058
+ ? { left: before, right: after }
11059
+ : { right: before, left: after };
11012
11060
  })
11013
11061
  });
11014
11062
  function asArray(val) { return (Array.isArray(val) ? val : [val]); }
@@ -11250,7 +11298,8 @@ const lineNumberGutter = /*@__PURE__*/activeGutters.compute([lineNumberConfig],
11250
11298
  let max = formatNumber(update.view, maxLineNumber(update.view.state.doc.lines));
11251
11299
  return max == spacer.number ? spacer : new NumberMarker(max);
11252
11300
  },
11253
- domEventHandlers: state.facet(lineNumberConfig).domEventHandlers
11301
+ domEventHandlers: state.facet(lineNumberConfig).domEventHandlers,
11302
+ side: "before"
11254
11303
  }));
11255
11304
  /**
11256
11305
  Create a line number gutter extension.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.37.1",
3
+ "version": "6.38.0",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",