@codemirror/view 6.33.0 → 6.34.1

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,29 @@
1
+ ## 6.34.1 (2024-09-27)
2
+
3
+ ### Bug fixes
4
+
5
+ Avoid a stack overflow that could happen when updating a line with a lot of text tokens.
6
+
7
+ Improve the way enormously long (non-wrapped) lines are displayed by making sure they stay shorter than the maximal pixel size the browser's CSS engine can handle.
8
+
9
+ ## 6.34.0 (2024-09-25)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix an issue where the dots past the wrapping point were displayed incorrectly when using `highlightWhitespace` with a wrapped sequence of spaces.
14
+
15
+ Improve performance of documents displaying lots of highlighted spaces by using a CSS background instead of pseudo-element.
16
+
17
+ ### New features
18
+
19
+ `placeholder` now allows a function that constructs the placedholder DOM to be passed in, and uses `cloneNode` when a raw element is passed in, to prevent adding the same element to multiple editors.
20
+
21
+ ## 6.33.1 (2024-08-30)
22
+
23
+ ### Bug fixes
24
+
25
+ Work around odd behavior in Chrome's newly supported `caretPositionFromPoint` method, which could cause CodeMirror to crash with a null dereference.
26
+
1
27
  ## 6.33.0 (2024-08-24)
2
28
 
3
29
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -20,12 +20,6 @@ function getSelection(root) {
20
20
  function contains(dom, node) {
21
21
  return node ? dom == node || dom.contains(node.nodeType != 1 ? node.parentNode : node) : false;
22
22
  }
23
- function deepActiveElement(doc) {
24
- let elt = doc.activeElement;
25
- while (elt && elt.shadowRoot)
26
- elt = elt.shadowRoot.activeElement;
27
- return elt;
28
- }
29
23
  function hasSelection(dom, selection) {
30
24
  if (!selection.anchorNode)
31
25
  return false;
@@ -567,7 +561,10 @@ class ContentView {
567
561
  if (child.parent == this && children.indexOf(child) < 0)
568
562
  child.destroy();
569
563
  }
570
- this.children.splice(from, to - from, ...children);
564
+ if (children.length < 250)
565
+ this.children.splice(from, to - from, ...children);
566
+ else
567
+ this.children = [].concat(this.children.slice(0, from), children, this.children.slice(to));
571
568
  for (let i = 0; i < children.length; i++)
572
569
  children[i].setParent(this);
573
570
  }
@@ -3581,6 +3578,11 @@ function posAtCoords(view, coords, precise, bias = -1) {
3581
3578
  node = undefined;
3582
3579
  }
3583
3580
  }
3581
+ // Chrome will return offsets into <input> elements without child
3582
+ // nodes, which will lead to a null deref below, so clip the
3583
+ // offset to the node size.
3584
+ if (node)
3585
+ offset = Math.min(maxOffset(node), offset);
3584
3586
  }
3585
3587
  // No luck, do our own (potentially expensive) search
3586
3588
  if (!node || !view.docView.dom.contains(node)) {
@@ -4130,7 +4132,6 @@ function selectionFromPoints(points, base) {
4130
4132
  return anchor > -1 && head > -1 ? state.EditorSelection.single(anchor + base, head + base) : null;
4131
4133
  }
4132
4134
 
4133
- // This will also be where dragging info and such goes
4134
4135
  class InputState {
4135
4136
  setSelectionOrigin(origin) {
4136
4137
  this.lastSelectionOrigin = origin;
@@ -5772,10 +5773,11 @@ function fullPixelRange(dom, paddingTop) {
5772
5773
  // lines within the viewport, as a kludge to keep the editor
5773
5774
  // responsive when a ridiculously long line is loaded into it.
5774
5775
  class LineGap {
5775
- constructor(from, to, size) {
5776
+ constructor(from, to, size, displaySize) {
5776
5777
  this.from = from;
5777
5778
  this.to = to;
5778
5779
  this.size = size;
5780
+ this.displaySize = displaySize;
5779
5781
  }
5780
5782
  static same(a, b) {
5781
5783
  if (a.length != b.length)
@@ -5789,7 +5791,7 @@ class LineGap {
5789
5791
  }
5790
5792
  draw(viewState, wrapping) {
5791
5793
  return Decoration.replace({
5792
- widget: new LineGapWidget(this.size * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5794
+ widget: new LineGapWidget(this.displaySize * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5793
5795
  }).range(this.from, this.to);
5794
5796
  }
5795
5797
  }
@@ -6091,7 +6093,7 @@ class ViewState {
6091
6093
  let mapped = [];
6092
6094
  for (let gap of gaps)
6093
6095
  if (!changes.touchesRange(gap.from, gap.to))
6094
- mapped.push(new LineGap(changes.mapPos(gap.from), changes.mapPos(gap.to), gap.size));
6096
+ mapped.push(new LineGap(changes.mapPos(gap.from), changes.mapPos(gap.to), gap.size, gap.displaySize));
6095
6097
  return mapped;
6096
6098
  }
6097
6099
  // Computes positions in the viewport where the start or end of a
@@ -6132,7 +6134,9 @@ class ViewState {
6132
6134
  if (lineStart > from)
6133
6135
  to = lineStart;
6134
6136
  }
6135
- gap = new LineGap(from, to, this.gapSize(line, from, to, structure));
6137
+ let size = this.gapSize(line, from, to, structure);
6138
+ let displaySize = wrapping || size < 2000000 /* VP.MaxHorizGap */ ? size : 2000000 /* VP.MaxHorizGap */;
6139
+ gap = new LineGap(from, to, size, displaySize);
6136
6140
  }
6137
6141
  gaps.push(gap);
6138
6142
  };
@@ -6163,16 +6167,24 @@ class ViewState {
6163
6167
  else {
6164
6168
  let totalWidth = structure.total * this.heightOracle.charWidth;
6165
6169
  let marginWidth = margin * this.heightOracle.charWidth;
6170
+ let horizOffset = 0;
6171
+ if (totalWidth > 2000000 /* VP.MaxHorizGap */)
6172
+ for (let old of current) {
6173
+ if (old.from >= line.from && old.from < line.to && old.size != old.displaySize &&
6174
+ old.from * this.heightOracle.charWidth + horizOffset < this.pixelViewport.left)
6175
+ horizOffset = old.size - old.displaySize;
6176
+ }
6177
+ let pxLeft = this.pixelViewport.left + horizOffset, pxRight = this.pixelViewport.right + horizOffset;
6166
6178
  let left, right;
6167
6179
  if (target != null) {
6168
6180
  let targetFrac = findFraction(structure, target);
6169
- let spaceFrac = ((this.pixelViewport.right - this.pixelViewport.left) / 2 + marginWidth) / totalWidth;
6181
+ let spaceFrac = ((pxRight - pxLeft) / 2 + marginWidth) / totalWidth;
6170
6182
  left = targetFrac - spaceFrac;
6171
6183
  right = targetFrac + spaceFrac;
6172
6184
  }
6173
6185
  else {
6174
- left = (this.pixelViewport.left - marginWidth) / totalWidth;
6175
- right = (this.pixelViewport.right + marginWidth) / totalWidth;
6186
+ left = (pxLeft - marginWidth) / totalWidth;
6187
+ right = (pxRight + marginWidth) / totalWidth;
6176
6188
  }
6177
6189
  viewFrom = findPosition(structure, left);
6178
6190
  viewTo = findPosition(structure, right);
@@ -6574,11 +6586,9 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6574
6586
  display: "inline-block",
6575
6587
  verticalAlign: "top",
6576
6588
  },
6577
- ".cm-highlightSpace:before": {
6578
- content: "attr(data-display)",
6579
- position: "absolute",
6580
- pointerEvents: "none",
6581
- color: "#888"
6589
+ ".cm-highlightSpace": {
6590
+ backgroundImage: "radial-gradient(circle at 50% 55%, #aaa 20%, transparent 5%)",
6591
+ backgroundPosition: "center",
6582
6592
  },
6583
6593
  ".cm-highlightTab": {
6584
6594
  backgroundImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="20"><path stroke="%23888" stroke-width="1" fill="none" d="M1 10H196L190 5M190 15L196 10M197 4L197 16"/></svg>')`,
@@ -6776,7 +6786,7 @@ class DOMObserver {
6776
6786
  if (!this.readSelectionRange() || this.delayedAndroidKey)
6777
6787
  return;
6778
6788
  let { view } = this, sel = this.selectionRange;
6779
- if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
6789
+ if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(this.dom, sel))
6780
6790
  return;
6781
6791
  let context = sel.anchorNode && view.docView.nearest(sel.anchorNode);
6782
6792
  if (context && context.ignoreEvent(event)) {
@@ -6804,7 +6814,7 @@ class DOMObserver {
6804
6814
  if (!selection)
6805
6815
  return false;
6806
6816
  let range = browser.safari && view.root.nodeType == 11 &&
6807
- deepActiveElement(this.dom.ownerDocument) == this.dom &&
6817
+ view.root.activeElement == this.dom &&
6808
6818
  safariSelectionRangeHack(this.view, selection) || selection;
6809
6819
  if (!range || this.selectionRange.eq(range))
6810
6820
  return false;
@@ -9492,11 +9502,13 @@ class Placeholder extends WidgetType {
9492
9502
  super();
9493
9503
  this.content = content;
9494
9504
  }
9495
- toDOM() {
9505
+ toDOM(view) {
9496
9506
  let wrap = document.createElement("span");
9497
9507
  wrap.className = "cm-placeholder";
9498
9508
  wrap.style.pointerEvents = "none";
9499
- wrap.appendChild(typeof this.content == "string" ? document.createTextNode(this.content) : this.content);
9509
+ wrap.appendChild(typeof this.content == "string" ? document.createTextNode(this.content) :
9510
+ typeof this.content == "function" ? this.content(view) :
9511
+ this.content.cloneNode(true));
9500
9512
  if (typeof this.content == "string")
9501
9513
  wrap.setAttribute("aria-label", "placeholder " + this.content);
9502
9514
  else
@@ -11031,20 +11043,6 @@ function highlightActiveLineGutter() {
11031
11043
  return activeLineGutterHighlighter;
11032
11044
  }
11033
11045
 
11034
- const WhitespaceDeco = new Map();
11035
- function getWhitespaceDeco(space) {
11036
- let deco = WhitespaceDeco.get(space);
11037
- if (!deco)
11038
- WhitespaceDeco.set(space, deco = Decoration.mark({
11039
- attributes: space === "\t" ? {
11040
- class: "cm-highlightTab",
11041
- } : {
11042
- class: "cm-highlightSpace",
11043
- "data-display": space.replace(/ /g, "·")
11044
- }
11045
- }));
11046
- return deco;
11047
- }
11048
11046
  function matcher(decorator) {
11049
11047
  return ViewPlugin.define(view => ({
11050
11048
  decorations: decorator.createDeco(view),
@@ -11055,9 +11053,11 @@ function matcher(decorator) {
11055
11053
  decorations: v => v.decorations
11056
11054
  });
11057
11055
  }
11056
+ const tabDeco = Decoration.mark({ class: "cm-highlightTab" });
11057
+ const spaceDeco = Decoration.mark({ class: "cm-highlightSpace" });
11058
11058
  const whitespaceHighlighter = matcher(new MatchDecorator({
11059
- regexp: /\t| +/g,
11060
- decoration: match => getWhitespaceDeco(match[0]),
11059
+ regexp: /\t| /g,
11060
+ decoration: match => match[0] == "\t" ? tabDeco : spaceDeco,
11061
11061
  boundary: /\S/,
11062
11062
  }));
11063
11063
  /**
package/dist/index.d.cts CHANGED
@@ -1585,7 +1585,7 @@ declare function highlightActiveLine(): Extension;
1585
1585
  Extension that enables a placeholder—a piece of example content
1586
1586
  to show when the editor is empty.
1587
1587
  */
1588
- declare function placeholder(content: string | HTMLElement): Extension;
1588
+ declare function placeholder(content: string | HTMLElement | ((view: EditorView) => HTMLElement)): Extension;
1589
1589
 
1590
1590
  /**
1591
1591
  Markers shown in a [layer](https://codemirror.net/6/docs/ref/#view.layer) must conform to this
package/dist/index.d.ts CHANGED
@@ -1585,7 +1585,7 @@ declare function highlightActiveLine(): Extension;
1585
1585
  Extension that enables a placeholder—a piece of example content
1586
1586
  to show when the editor is empty.
1587
1587
  */
1588
- declare function placeholder(content: string | HTMLElement): Extension;
1588
+ declare function placeholder(content: string | HTMLElement | ((view: EditorView) => HTMLElement)): Extension;
1589
1589
 
1590
1590
  /**
1591
1591
  Markers shown in a [layer](https://codemirror.net/6/docs/ref/#view.layer) must conform to this
package/dist/index.js CHANGED
@@ -18,12 +18,6 @@ function getSelection(root) {
18
18
  function contains(dom, node) {
19
19
  return node ? dom == node || dom.contains(node.nodeType != 1 ? node.parentNode : node) : false;
20
20
  }
21
- function deepActiveElement(doc) {
22
- let elt = doc.activeElement;
23
- while (elt && elt.shadowRoot)
24
- elt = elt.shadowRoot.activeElement;
25
- return elt;
26
- }
27
21
  function hasSelection(dom, selection) {
28
22
  if (!selection.anchorNode)
29
23
  return false;
@@ -565,7 +559,10 @@ class ContentView {
565
559
  if (child.parent == this && children.indexOf(child) < 0)
566
560
  child.destroy();
567
561
  }
568
- this.children.splice(from, to - from, ...children);
562
+ if (children.length < 250)
563
+ this.children.splice(from, to - from, ...children);
564
+ else
565
+ this.children = [].concat(this.children.slice(0, from), children, this.children.slice(to));
569
566
  for (let i = 0; i < children.length; i++)
570
567
  children[i].setParent(this);
571
568
  }
@@ -3577,6 +3574,11 @@ function posAtCoords(view, coords, precise, bias = -1) {
3577
3574
  node = undefined;
3578
3575
  }
3579
3576
  }
3577
+ // Chrome will return offsets into <input> elements without child
3578
+ // nodes, which will lead to a null deref below, so clip the
3579
+ // offset to the node size.
3580
+ if (node)
3581
+ offset = Math.min(maxOffset(node), offset);
3580
3582
  }
3581
3583
  // No luck, do our own (potentially expensive) search
3582
3584
  if (!node || !view.docView.dom.contains(node)) {
@@ -4126,7 +4128,6 @@ function selectionFromPoints(points, base) {
4126
4128
  return anchor > -1 && head > -1 ? EditorSelection.single(anchor + base, head + base) : null;
4127
4129
  }
4128
4130
 
4129
- // This will also be where dragging info and such goes
4130
4131
  class InputState {
4131
4132
  setSelectionOrigin(origin) {
4132
4133
  this.lastSelectionOrigin = origin;
@@ -5767,10 +5768,11 @@ function fullPixelRange(dom, paddingTop) {
5767
5768
  // lines within the viewport, as a kludge to keep the editor
5768
5769
  // responsive when a ridiculously long line is loaded into it.
5769
5770
  class LineGap {
5770
- constructor(from, to, size) {
5771
+ constructor(from, to, size, displaySize) {
5771
5772
  this.from = from;
5772
5773
  this.to = to;
5773
5774
  this.size = size;
5775
+ this.displaySize = displaySize;
5774
5776
  }
5775
5777
  static same(a, b) {
5776
5778
  if (a.length != b.length)
@@ -5784,7 +5786,7 @@ class LineGap {
5784
5786
  }
5785
5787
  draw(viewState, wrapping) {
5786
5788
  return Decoration.replace({
5787
- widget: new LineGapWidget(this.size * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5789
+ widget: new LineGapWidget(this.displaySize * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5788
5790
  }).range(this.from, this.to);
5789
5791
  }
5790
5792
  }
@@ -6086,7 +6088,7 @@ class ViewState {
6086
6088
  let mapped = [];
6087
6089
  for (let gap of gaps)
6088
6090
  if (!changes.touchesRange(gap.from, gap.to))
6089
- mapped.push(new LineGap(changes.mapPos(gap.from), changes.mapPos(gap.to), gap.size));
6091
+ mapped.push(new LineGap(changes.mapPos(gap.from), changes.mapPos(gap.to), gap.size, gap.displaySize));
6090
6092
  return mapped;
6091
6093
  }
6092
6094
  // Computes positions in the viewport where the start or end of a
@@ -6127,7 +6129,9 @@ class ViewState {
6127
6129
  if (lineStart > from)
6128
6130
  to = lineStart;
6129
6131
  }
6130
- gap = new LineGap(from, to, this.gapSize(line, from, to, structure));
6132
+ let size = this.gapSize(line, from, to, structure);
6133
+ let displaySize = wrapping || size < 2000000 /* VP.MaxHorizGap */ ? size : 2000000 /* VP.MaxHorizGap */;
6134
+ gap = new LineGap(from, to, size, displaySize);
6131
6135
  }
6132
6136
  gaps.push(gap);
6133
6137
  };
@@ -6158,16 +6162,24 @@ class ViewState {
6158
6162
  else {
6159
6163
  let totalWidth = structure.total * this.heightOracle.charWidth;
6160
6164
  let marginWidth = margin * this.heightOracle.charWidth;
6165
+ let horizOffset = 0;
6166
+ if (totalWidth > 2000000 /* VP.MaxHorizGap */)
6167
+ for (let old of current) {
6168
+ if (old.from >= line.from && old.from < line.to && old.size != old.displaySize &&
6169
+ old.from * this.heightOracle.charWidth + horizOffset < this.pixelViewport.left)
6170
+ horizOffset = old.size - old.displaySize;
6171
+ }
6172
+ let pxLeft = this.pixelViewport.left + horizOffset, pxRight = this.pixelViewport.right + horizOffset;
6161
6173
  let left, right;
6162
6174
  if (target != null) {
6163
6175
  let targetFrac = findFraction(structure, target);
6164
- let spaceFrac = ((this.pixelViewport.right - this.pixelViewport.left) / 2 + marginWidth) / totalWidth;
6176
+ let spaceFrac = ((pxRight - pxLeft) / 2 + marginWidth) / totalWidth;
6165
6177
  left = targetFrac - spaceFrac;
6166
6178
  right = targetFrac + spaceFrac;
6167
6179
  }
6168
6180
  else {
6169
- left = (this.pixelViewport.left - marginWidth) / totalWidth;
6170
- right = (this.pixelViewport.right + marginWidth) / totalWidth;
6181
+ left = (pxLeft - marginWidth) / totalWidth;
6182
+ right = (pxRight + marginWidth) / totalWidth;
6171
6183
  }
6172
6184
  viewFrom = findPosition(structure, left);
6173
6185
  viewTo = findPosition(structure, right);
@@ -6569,11 +6581,9 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6569
6581
  display: "inline-block",
6570
6582
  verticalAlign: "top",
6571
6583
  },
6572
- ".cm-highlightSpace:before": {
6573
- content: "attr(data-display)",
6574
- position: "absolute",
6575
- pointerEvents: "none",
6576
- color: "#888"
6584
+ ".cm-highlightSpace": {
6585
+ backgroundImage: "radial-gradient(circle at 50% 55%, #aaa 20%, transparent 5%)",
6586
+ backgroundPosition: "center",
6577
6587
  },
6578
6588
  ".cm-highlightTab": {
6579
6589
  backgroundImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="20"><path stroke="%23888" stroke-width="1" fill="none" d="M1 10H196L190 5M190 15L196 10M197 4L197 16"/></svg>')`,
@@ -6771,7 +6781,7 @@ class DOMObserver {
6771
6781
  if (!this.readSelectionRange() || this.delayedAndroidKey)
6772
6782
  return;
6773
6783
  let { view } = this, sel = this.selectionRange;
6774
- if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
6784
+ if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(this.dom, sel))
6775
6785
  return;
6776
6786
  let context = sel.anchorNode && view.docView.nearest(sel.anchorNode);
6777
6787
  if (context && context.ignoreEvent(event)) {
@@ -6799,7 +6809,7 @@ class DOMObserver {
6799
6809
  if (!selection)
6800
6810
  return false;
6801
6811
  let range = browser.safari && view.root.nodeType == 11 &&
6802
- deepActiveElement(this.dom.ownerDocument) == this.dom &&
6812
+ view.root.activeElement == this.dom &&
6803
6813
  safariSelectionRangeHack(this.view, selection) || selection;
6804
6814
  if (!range || this.selectionRange.eq(range))
6805
6815
  return false;
@@ -9487,11 +9497,13 @@ class Placeholder extends WidgetType {
9487
9497
  super();
9488
9498
  this.content = content;
9489
9499
  }
9490
- toDOM() {
9500
+ toDOM(view) {
9491
9501
  let wrap = document.createElement("span");
9492
9502
  wrap.className = "cm-placeholder";
9493
9503
  wrap.style.pointerEvents = "none";
9494
- wrap.appendChild(typeof this.content == "string" ? document.createTextNode(this.content) : this.content);
9504
+ wrap.appendChild(typeof this.content == "string" ? document.createTextNode(this.content) :
9505
+ typeof this.content == "function" ? this.content(view) :
9506
+ this.content.cloneNode(true));
9495
9507
  if (typeof this.content == "string")
9496
9508
  wrap.setAttribute("aria-label", "placeholder " + this.content);
9497
9509
  else
@@ -11026,20 +11038,6 @@ function highlightActiveLineGutter() {
11026
11038
  return activeLineGutterHighlighter;
11027
11039
  }
11028
11040
 
11029
- const WhitespaceDeco = /*@__PURE__*/new Map();
11030
- function getWhitespaceDeco(space) {
11031
- let deco = WhitespaceDeco.get(space);
11032
- if (!deco)
11033
- WhitespaceDeco.set(space, deco = Decoration.mark({
11034
- attributes: space === "\t" ? {
11035
- class: "cm-highlightTab",
11036
- } : {
11037
- class: "cm-highlightSpace",
11038
- "data-display": space.replace(/ /g, "·")
11039
- }
11040
- }));
11041
- return deco;
11042
- }
11043
11041
  function matcher(decorator) {
11044
11042
  return ViewPlugin.define(view => ({
11045
11043
  decorations: decorator.createDeco(view),
@@ -11050,9 +11048,11 @@ function matcher(decorator) {
11050
11048
  decorations: v => v.decorations
11051
11049
  });
11052
11050
  }
11051
+ const tabDeco = /*@__PURE__*/Decoration.mark({ class: "cm-highlightTab" });
11052
+ const spaceDeco = /*@__PURE__*/Decoration.mark({ class: "cm-highlightSpace" });
11053
11053
  const whitespaceHighlighter = /*@__PURE__*/matcher(/*@__PURE__*/new MatchDecorator({
11054
- regexp: /\t| +/g,
11055
- decoration: match => getWhitespaceDeco(match[0]),
11054
+ regexp: /\t| /g,
11055
+ decoration: match => match[0] == "\t" ? tabDeco : spaceDeco,
11056
11056
  boundary: /\S/,
11057
11057
  }));
11058
11058
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.33.0",
3
+ "version": "6.34.1",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",