@codemirror/view 6.26.3 → 6.26.4-edit-context

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.
Files changed (3) hide show
  1. package/dist/index.cjs +225 -34
  2. package/dist/index.js +225 -34
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2368,6 +2368,7 @@ class ScrollTarget {
2368
2368
  }
2369
2369
  }
2370
2370
  const scrollIntoView = state.StateEffect.define({ map: (t, ch) => t.map(ch) });
2371
+ const setEditContextFormatting = state.StateEffect.define();
2371
2372
  /**
2372
2373
  Log or report an unhandled exception in client code. Should
2373
2374
  probably only be used by extension code that allows client code to
@@ -2704,10 +2705,11 @@ class DocView extends ContentView {
2704
2705
  super();
2705
2706
  this.view = view;
2706
2707
  this.decorations = [];
2707
- this.dynamicDecorationMap = [];
2708
+ this.dynamicDecorationMap = [false];
2708
2709
  this.domChanged = null;
2709
2710
  this.hasComposition = null;
2710
2711
  this.markedForComposition = new Set;
2712
+ this.editContextFormatting = Decoration.none;
2711
2713
  this.lastCompositionAfterCursor = false;
2712
2714
  // Track a minimum width for the editor. When measuring sizes in
2713
2715
  // measureVisibleLineHeights, this is updated to point at the width
@@ -2746,8 +2748,9 @@ class DocView extends ContentView {
2746
2748
  this.minWidthTo = update.changes.mapPos(this.minWidthTo, 1);
2747
2749
  }
2748
2750
  }
2751
+ this.updateEditContextFormatting(update);
2749
2752
  let readCompositionAt = -1;
2750
- if (this.view.inputState.composing >= 0) {
2753
+ if (this.view.inputState.composing >= 0 && !this.view.observer.editContext) {
2751
2754
  if ((_a = this.domChanged) === null || _a === void 0 ? void 0 : _a.newSel)
2752
2755
  readCompositionAt = this.domChanged.newSel.head;
2753
2756
  else if (!touchesComposition(update.changes, this.hasComposition) && !update.selectionSet)
@@ -2855,6 +2858,14 @@ class DocView extends ContentView {
2855
2858
  if (composition)
2856
2859
  this.fixCompositionDOM(composition);
2857
2860
  }
2861
+ updateEditContextFormatting(update) {
2862
+ this.editContextFormatting = this.editContextFormatting.map(update.changes);
2863
+ for (let tr of update.transactions)
2864
+ for (let effect of tr.effects)
2865
+ if (effect.is(setEditContextFormatting)) {
2866
+ this.editContextFormatting = effect.value;
2867
+ }
2868
+ }
2858
2869
  compositionView(composition) {
2859
2870
  let cur = new TextView(composition.text.nodeValue);
2860
2871
  cur.flags |= 8 /* ViewFlag.Composition */;
@@ -3180,7 +3191,7 @@ class DocView extends ContentView {
3180
3191
  return Decoration.set(deco);
3181
3192
  }
3182
3193
  updateDeco() {
3183
- let i = 0;
3194
+ let i = 1;
3184
3195
  let allDeco = this.view.state.facet(decorations).map(d => {
3185
3196
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3186
3197
  return dynamic ? d(this.view) : d;
@@ -3196,6 +3207,7 @@ class DocView extends ContentView {
3196
3207
  allDeco.push(state.RangeSet.join(outerDeco));
3197
3208
  }
3198
3209
  this.decorations = [
3210
+ this.editContextFormatting,
3199
3211
  ...allDeco,
3200
3212
  this.computeBlockGapDeco(),
3201
3213
  this.view.viewState.lineGapDeco
@@ -3882,6 +3894,7 @@ class InputState {
3882
3894
  this.mouseSelection = mouseSelection;
3883
3895
  }
3884
3896
  update(update) {
3897
+ this.view.observer.update(update);
3885
3898
  if (this.mouseSelection)
3886
3899
  this.mouseSelection.update(update);
3887
3900
  if (this.draggedContent && update.docChanged)
@@ -4472,6 +4485,8 @@ observers.blur = view => {
4472
4485
  updateForFocusChange(view);
4473
4486
  };
4474
4487
  observers.compositionstart = observers.compositionupdate = view => {
4488
+ if (view.observer.editContext)
4489
+ return; // Composition handled by edit context
4475
4490
  if (view.inputState.compositionFirstChange == null)
4476
4491
  view.inputState.compositionFirstChange = true;
4477
4492
  if (view.inputState.composing < 0) {
@@ -4480,6 +4495,8 @@ observers.compositionstart = observers.compositionupdate = view => {
4480
4495
  }
4481
4496
  };
4482
4497
  observers.compositionend = view => {
4498
+ if (view.observer.editContext)
4499
+ return; // Composition handled by edit context
4483
4500
  view.inputState.composing = -1;
4484
4501
  view.inputState.compositionEndedAt = Date.now();
4485
4502
  view.inputState.compositionPendingKey = true;
@@ -6356,35 +6373,7 @@ function applyDOMChange(view, domChange) {
6356
6373
  change = { from: sel.from, to: sel.to, insert: state.Text.of([" "]) };
6357
6374
  }
6358
6375
  if (change) {
6359
- if (browser.ios && view.inputState.flushIOSKey(change))
6360
- return true;
6361
- // Android browsers don't fire reasonable key events for enter,
6362
- // backspace, or delete. So this detects changes that look like
6363
- // they're caused by those keys, and reinterprets them as key
6364
- // events. (Some of these keys are also handled by beforeinput
6365
- // events and the pendingAndroidKey mechanism, but that's not
6366
- // reliable in all situations.)
6367
- if (browser.android &&
6368
- ((change.to == sel.to &&
6369
- // GBoard will sometimes remove a space it just inserted
6370
- // after a completion when you press enter
6371
- (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6372
- change.insert.length == 1 && change.insert.lines == 2 &&
6373
- dispatchKey(view.contentDOM, "Enter", 13)) ||
6374
- ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6375
- lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6376
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
6377
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6378
- dispatchKey(view.contentDOM, "Delete", 46))))
6379
- return true;
6380
- let text = change.insert.toString();
6381
- if (view.inputState.composing >= 0)
6382
- view.inputState.composing++;
6383
- let defaultTr;
6384
- let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6385
- if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6386
- view.dispatch(defaultInsert());
6387
- return true;
6376
+ return applyDOMChangeInner(view, change, newSel, lastKey);
6388
6377
  }
6389
6378
  else if (newSel && !newSel.main.eq(sel)) {
6390
6379
  let scrollIntoView = false, userEvent = "select";
@@ -6400,6 +6389,38 @@ function applyDOMChange(view, domChange) {
6400
6389
  return false;
6401
6390
  }
6402
6391
  }
6392
+ function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
6393
+ if (browser.ios && view.inputState.flushIOSKey(change))
6394
+ return true;
6395
+ let sel = view.state.selection.main;
6396
+ // Android browsers don't fire reasonable key events for enter,
6397
+ // backspace, or delete. So this detects changes that look like
6398
+ // they're caused by those keys, and reinterprets them as key
6399
+ // events. (Some of these keys are also handled by beforeinput
6400
+ // events and the pendingAndroidKey mechanism, but that's not
6401
+ // reliable in all situations.)
6402
+ if (browser.android &&
6403
+ ((change.to == sel.to &&
6404
+ // GBoard will sometimes remove a space it just inserted
6405
+ // after a completion when you press enter
6406
+ (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6407
+ change.insert.length == 1 && change.insert.lines == 2 &&
6408
+ dispatchKey(view.contentDOM, "Enter", 13)) ||
6409
+ ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6410
+ lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6411
+ dispatchKey(view.contentDOM, "Backspace", 8)) ||
6412
+ (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6413
+ dispatchKey(view.contentDOM, "Delete", 46))))
6414
+ return true;
6415
+ let text = change.insert.toString();
6416
+ if (view.inputState.composing >= 0)
6417
+ view.inputState.composing++;
6418
+ let defaultTr;
6419
+ let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6420
+ if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6421
+ view.dispatch(defaultInsert());
6422
+ return true;
6423
+ }
6403
6424
  function applyDefaultInsert(view, change, newSel) {
6404
6425
  let tr, startState = view.state, sel = startState.selection.main;
6405
6426
  if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
@@ -6526,6 +6547,7 @@ class DOMObserver {
6526
6547
  constructor(view) {
6527
6548
  this.view = view;
6528
6549
  this.active = false;
6550
+ this.editContext = null;
6529
6551
  // The known selection. Kept in our own object, as opposed to just
6530
6552
  // directly accessing the selection because:
6531
6553
  // - Safari doesn't report the right selection in shadow DOM
@@ -6570,6 +6592,10 @@ class DOMObserver {
6570
6592
  else
6571
6593
  this.flush();
6572
6594
  });
6595
+ if (window.EditContext) {
6596
+ this.editContext = new EditContextManager(view);
6597
+ view.contentDOM.editContext = this.editContext.editContext;
6598
+ }
6573
6599
  if (useCharData)
6574
6600
  this.onCharData = (event) => {
6575
6601
  this.queue.push({ target: event.target,
@@ -6620,6 +6646,8 @@ class DOMObserver {
6620
6646
  onScroll(e) {
6621
6647
  if (this.intersecting)
6622
6648
  this.flush(false);
6649
+ if (this.editContext)
6650
+ this.view.requestMeasure(this.editContext.measureReq);
6623
6651
  this.onScrollChanged(e);
6624
6652
  }
6625
6653
  onResize() {
@@ -6928,6 +6956,10 @@ class DOMObserver {
6928
6956
  win.removeEventListener("beforeprint", this.onPrint);
6929
6957
  win.document.removeEventListener("selectionchange", this.onSelectionChange);
6930
6958
  }
6959
+ update(update) {
6960
+ if (this.editContext)
6961
+ this.editContext.update(update);
6962
+ }
6931
6963
  destroy() {
6932
6964
  var _a, _b, _c;
6933
6965
  this.stop();
@@ -6987,6 +7019,161 @@ function safariSelectionRangeHack(view, selection) {
6987
7019
  view.contentDOM.removeEventListener("beforeinput", read, true);
6988
7020
  return found ? buildSelectionRangeFromRange(view, found) : null;
6989
7021
  }
7022
+ class EditContextManager {
7023
+ constructor(view) {
7024
+ // The document window for which the text in the context is
7025
+ // maintained. For large documents, this may be smaller than the
7026
+ // editor document. This window always includes the selection head.
7027
+ this.from = 0;
7028
+ this.to = 0;
7029
+ // When applying a transaction, this is used to compare the change
7030
+ // made to the context content to the change in the transaction in
7031
+ // order to make the minimal changes to the context (since touching
7032
+ // that sometimes breaks series of multiple edits made for a single
7033
+ // user action on some Android keyboards)
7034
+ this.pendingContextChange = null;
7035
+ this.resetRange(view.state);
7036
+ let context = this.editContext = new window.EditContext({
7037
+ text: view.state.doc.sliceString(this.from, this.to),
7038
+ selectionStart: this.toContextPos(Math.max(this.from, Math.min(this.to, view.state.selection.main.anchor))),
7039
+ selectionEnd: this.toContextPos(view.state.selection.main.head)
7040
+ });
7041
+ context.addEventListener("textupdate", e => {
7042
+ let { anchor } = view.state.selection.main;
7043
+ let change = { from: this.toEditorPos(e.updateRangeStart),
7044
+ to: this.toEditorPos(e.updateRangeEnd),
7045
+ insert: state.Text.of(e.text.split("\n")) };
7046
+ // If the window doesn't include the anchor, assume changes
7047
+ // adjacent to a side go up to the anchor.
7048
+ if (change.from == this.from && anchor < this.from)
7049
+ change.from = anchor;
7050
+ else if (change.to == this.to && anchor > this.to)
7051
+ change.to = anchor;
7052
+ // Edit context sometimes fire empty changes
7053
+ if (change.from == change.to && !change.insert.length)
7054
+ return;
7055
+ this.pendingContextChange = change;
7056
+ applyDOMChangeInner(view, change, state.EditorSelection.single(this.toEditorPos(e.selectionStart), this.toEditorPos(e.selectionEnd)));
7057
+ // If the transaction didn't flush our change, revert it so
7058
+ // that the context is in sync with the editor state again.
7059
+ if (this.pendingContextChange)
7060
+ this.revertPending(view.state);
7061
+ });
7062
+ context.addEventListener("characterboundsupdate", e => {
7063
+ let rects = [], prev = null;
7064
+ for (let i = this.toEditorPos(e.rangeStart), end = this.toEditorPos(e.rangeEnd); i < end; i++) {
7065
+ let rect = view.coordsForChar(i);
7066
+ prev = (rect && new DOMRect(rect.left, rect.right, rect.right - rect.left, rect.bottom - rect.top))
7067
+ || prev || new DOMRect;
7068
+ rects.push(prev);
7069
+ }
7070
+ context.updateCharacterBounds(e.rangeStart, rects);
7071
+ });
7072
+ context.addEventListener("textformatupdate", e => {
7073
+ let deco = [];
7074
+ for (let format of e.getTextFormats()) {
7075
+ let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
7076
+ if (lineStyle != "None" && thickness != "None") {
7077
+ let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
7078
+ deco.push(Decoration.mark({ attributes: { style } })
7079
+ .range(this.toEditorPos(format.rangeStart), this.toEditorPos(format.rangeEnd)));
7080
+ }
7081
+ }
7082
+ view.dispatch({ effects: setEditContextFormatting.of(Decoration.set(deco)) });
7083
+ });
7084
+ context.addEventListener("compositionstart", () => {
7085
+ if (view.inputState.composing < 0) {
7086
+ view.inputState.composing = 0;
7087
+ view.inputState.compositionFirstChange = true;
7088
+ }
7089
+ });
7090
+ context.addEventListener("compositionend", () => {
7091
+ view.inputState.composing = -1;
7092
+ view.inputState.compositionFirstChange = null;
7093
+ });
7094
+ this.measureReq = { read: view => {
7095
+ this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
7096
+ let sel = getSelection(view.root);
7097
+ if (sel && sel.rangeCount)
7098
+ this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
7099
+ } };
7100
+ }
7101
+ applyEdits(update) {
7102
+ let off = 0, abort = false, pending = this.pendingContextChange;
7103
+ update.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
7104
+ if (abort)
7105
+ return;
7106
+ let dLen = insert.length - (toA - fromA);
7107
+ if (pending && toA >= pending.to) {
7108
+ if (pending.from == fromA && pending.to == toA && pending.insert.eq(insert)) {
7109
+ pending = this.pendingContextChange = null; // Match
7110
+ off += dLen;
7111
+ return;
7112
+ }
7113
+ else { // Mismatch, revert
7114
+ pending = null;
7115
+ this.revertPending(update.state);
7116
+ }
7117
+ }
7118
+ fromA += off;
7119
+ toA += off;
7120
+ if (toA <= this.from) { // Before the window
7121
+ this.from += dLen;
7122
+ this.to += dLen;
7123
+ }
7124
+ else if (fromA < this.to) { // Overlaps with window
7125
+ if (fromA < this.from || toA > this.to || (this.to - this.from) + insert.length > 30000 /* CxVp.MaxSize */) {
7126
+ abort = true;
7127
+ return;
7128
+ }
7129
+ this.editContext.updateText(this.toContextPos(fromA), this.toContextPos(toA), insert.toString());
7130
+ this.to += dLen;
7131
+ }
7132
+ off += dLen;
7133
+ });
7134
+ if (pending && !abort)
7135
+ this.revertPending(update.state);
7136
+ return !abort;
7137
+ }
7138
+ update(update) {
7139
+ if (!this.applyEdits(update) || !this.rangeIsValid(update.state)) {
7140
+ this.pendingContextChange = null;
7141
+ this.resetRange(update.state);
7142
+ this.editContext.updateText(0, this.editContext.text.length, update.state.doc.sliceString(this.from, this.to));
7143
+ this.setSelection(update.state);
7144
+ }
7145
+ else if (update.docChanged || update.selectionSet) {
7146
+ this.setSelection(update.state);
7147
+ }
7148
+ if (update.geometryChanged || update.docChanged || update.selectionSet)
7149
+ update.view.requestMeasure(this.measureReq);
7150
+ }
7151
+ resetRange(state) {
7152
+ let { head } = state.selection.main;
7153
+ this.from = Math.max(0, head - 10000 /* CxVp.Margin */);
7154
+ this.to = Math.min(state.doc.length, head + 10000 /* CxVp.Margin */);
7155
+ }
7156
+ revertPending(state) {
7157
+ let pending = this.pendingContextChange;
7158
+ this.pendingContextChange = null;
7159
+ this.editContext.updateText(this.toContextPos(pending.from), this.toContextPos(pending.to + pending.insert.length), state.doc.sliceString(pending.from, pending.to));
7160
+ }
7161
+ setSelection(state) {
7162
+ let { main } = state.selection;
7163
+ let start = this.toContextPos(Math.max(this.from, Math.min(this.to, main.anchor)));
7164
+ let end = this.toContextPos(main.head);
7165
+ if (this.editContext.selectionStart != start || this.editContext.selectionEnd != end)
7166
+ this.editContext.updateSelection(start, end);
7167
+ }
7168
+ rangeIsValid(state) {
7169
+ let { head } = state.selection.main;
7170
+ return !(this.from > 0 && head - this.from < 500 /* CxVp.MinMargin */ ||
7171
+ this.to < state.doc.length && this.to - head < 500 /* CxVp.MinMargin */ ||
7172
+ this.to - this.from > 10000 /* CxVp.Margin */ * 3);
7173
+ }
7174
+ toEditorPos(contextPos) { return contextPos + this.from; }
7175
+ toContextPos(editorPos) { return editorPos - this.from; }
7176
+ }
6990
7177
 
6991
7178
  // The editor's update state machine looks something like this:
6992
7179
  //
@@ -8257,8 +8444,9 @@ function buildKeymap(bindings, platform = currentPlatform) {
8257
8444
  let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
8258
8445
  if (!scopeObj._any)
8259
8446
  scopeObj._any = { preventDefault: false, stopPropagation: false, run: [] };
8447
+ let { any } = b;
8260
8448
  for (let key in scopeObj)
8261
- scopeObj[key].run.push(b.any);
8449
+ scopeObj[key].run.push(view => any(view, currentKeyEvent));
8262
8450
  }
8263
8451
  let name = b[platform] || b.key;
8264
8452
  if (!name)
@@ -8271,7 +8459,9 @@ function buildKeymap(bindings, platform = currentPlatform) {
8271
8459
  }
8272
8460
  return bound;
8273
8461
  }
8462
+ let currentKeyEvent = null;
8274
8463
  function runHandlers(map, event, view, scope) {
8464
+ currentKeyEvent = event;
8275
8465
  let name = w3cKeyname.keyName(event);
8276
8466
  let charCode = state.codePointAt(name, 0), isChar = state.codePointSize(charCode) == name.length && name != " ";
8277
8467
  let prefix = "", handled = false, prevented = false, stopPropagation = false;
@@ -8288,7 +8478,7 @@ function runHandlers(map, event, view, scope) {
8288
8478
  for (let cmd of binding.run)
8289
8479
  if (!ran.has(cmd)) {
8290
8480
  ran.add(cmd);
8291
- if (cmd(view, event)) {
8481
+ if (cmd(view)) {
8292
8482
  if (binding.stopPropagation)
8293
8483
  stopPropagation = true;
8294
8484
  return true;
@@ -8330,6 +8520,7 @@ function runHandlers(map, event, view, scope) {
8330
8520
  handled = true;
8331
8521
  if (handled && stopPropagation)
8332
8522
  event.stopPropagation();
8523
+ currentKeyEvent = null;
8333
8524
  return handled;
8334
8525
  }
8335
8526
 
package/dist/index.js CHANGED
@@ -2364,6 +2364,7 @@ class ScrollTarget {
2364
2364
  }
2365
2365
  }
2366
2366
  const scrollIntoView = /*@__PURE__*/StateEffect.define({ map: (t, ch) => t.map(ch) });
2367
+ const setEditContextFormatting = /*@__PURE__*/StateEffect.define();
2367
2368
  /**
2368
2369
  Log or report an unhandled exception in client code. Should
2369
2370
  probably only be used by extension code that allows client code to
@@ -2700,10 +2701,11 @@ class DocView extends ContentView {
2700
2701
  super();
2701
2702
  this.view = view;
2702
2703
  this.decorations = [];
2703
- this.dynamicDecorationMap = [];
2704
+ this.dynamicDecorationMap = [false];
2704
2705
  this.domChanged = null;
2705
2706
  this.hasComposition = null;
2706
2707
  this.markedForComposition = new Set;
2708
+ this.editContextFormatting = Decoration.none;
2707
2709
  this.lastCompositionAfterCursor = false;
2708
2710
  // Track a minimum width for the editor. When measuring sizes in
2709
2711
  // measureVisibleLineHeights, this is updated to point at the width
@@ -2742,8 +2744,9 @@ class DocView extends ContentView {
2742
2744
  this.minWidthTo = update.changes.mapPos(this.minWidthTo, 1);
2743
2745
  }
2744
2746
  }
2747
+ this.updateEditContextFormatting(update);
2745
2748
  let readCompositionAt = -1;
2746
- if (this.view.inputState.composing >= 0) {
2749
+ if (this.view.inputState.composing >= 0 && !this.view.observer.editContext) {
2747
2750
  if ((_a = this.domChanged) === null || _a === void 0 ? void 0 : _a.newSel)
2748
2751
  readCompositionAt = this.domChanged.newSel.head;
2749
2752
  else if (!touchesComposition(update.changes, this.hasComposition) && !update.selectionSet)
@@ -2851,6 +2854,14 @@ class DocView extends ContentView {
2851
2854
  if (composition)
2852
2855
  this.fixCompositionDOM(composition);
2853
2856
  }
2857
+ updateEditContextFormatting(update) {
2858
+ this.editContextFormatting = this.editContextFormatting.map(update.changes);
2859
+ for (let tr of update.transactions)
2860
+ for (let effect of tr.effects)
2861
+ if (effect.is(setEditContextFormatting)) {
2862
+ this.editContextFormatting = effect.value;
2863
+ }
2864
+ }
2854
2865
  compositionView(composition) {
2855
2866
  let cur = new TextView(composition.text.nodeValue);
2856
2867
  cur.flags |= 8 /* ViewFlag.Composition */;
@@ -3176,7 +3187,7 @@ class DocView extends ContentView {
3176
3187
  return Decoration.set(deco);
3177
3188
  }
3178
3189
  updateDeco() {
3179
- let i = 0;
3190
+ let i = 1;
3180
3191
  let allDeco = this.view.state.facet(decorations).map(d => {
3181
3192
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3182
3193
  return dynamic ? d(this.view) : d;
@@ -3192,6 +3203,7 @@ class DocView extends ContentView {
3192
3203
  allDeco.push(RangeSet.join(outerDeco));
3193
3204
  }
3194
3205
  this.decorations = [
3206
+ this.editContextFormatting,
3195
3207
  ...allDeco,
3196
3208
  this.computeBlockGapDeco(),
3197
3209
  this.view.viewState.lineGapDeco
@@ -3878,6 +3890,7 @@ class InputState {
3878
3890
  this.mouseSelection = mouseSelection;
3879
3891
  }
3880
3892
  update(update) {
3893
+ this.view.observer.update(update);
3881
3894
  if (this.mouseSelection)
3882
3895
  this.mouseSelection.update(update);
3883
3896
  if (this.draggedContent && update.docChanged)
@@ -4468,6 +4481,8 @@ observers.blur = view => {
4468
4481
  updateForFocusChange(view);
4469
4482
  };
4470
4483
  observers.compositionstart = observers.compositionupdate = view => {
4484
+ if (view.observer.editContext)
4485
+ return; // Composition handled by edit context
4471
4486
  if (view.inputState.compositionFirstChange == null)
4472
4487
  view.inputState.compositionFirstChange = true;
4473
4488
  if (view.inputState.composing < 0) {
@@ -4476,6 +4491,8 @@ observers.compositionstart = observers.compositionupdate = view => {
4476
4491
  }
4477
4492
  };
4478
4493
  observers.compositionend = view => {
4494
+ if (view.observer.editContext)
4495
+ return; // Composition handled by edit context
4479
4496
  view.inputState.composing = -1;
4480
4497
  view.inputState.compositionEndedAt = Date.now();
4481
4498
  view.inputState.compositionPendingKey = true;
@@ -6351,35 +6368,7 @@ function applyDOMChange(view, domChange) {
6351
6368
  change = { from: sel.from, to: sel.to, insert: Text.of([" "]) };
6352
6369
  }
6353
6370
  if (change) {
6354
- if (browser.ios && view.inputState.flushIOSKey(change))
6355
- return true;
6356
- // Android browsers don't fire reasonable key events for enter,
6357
- // backspace, or delete. So this detects changes that look like
6358
- // they're caused by those keys, and reinterprets them as key
6359
- // events. (Some of these keys are also handled by beforeinput
6360
- // events and the pendingAndroidKey mechanism, but that's not
6361
- // reliable in all situations.)
6362
- if (browser.android &&
6363
- ((change.to == sel.to &&
6364
- // GBoard will sometimes remove a space it just inserted
6365
- // after a completion when you press enter
6366
- (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6367
- change.insert.length == 1 && change.insert.lines == 2 &&
6368
- dispatchKey(view.contentDOM, "Enter", 13)) ||
6369
- ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6370
- lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6371
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
6372
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6373
- dispatchKey(view.contentDOM, "Delete", 46))))
6374
- return true;
6375
- let text = change.insert.toString();
6376
- if (view.inputState.composing >= 0)
6377
- view.inputState.composing++;
6378
- let defaultTr;
6379
- let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6380
- if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6381
- view.dispatch(defaultInsert());
6382
- return true;
6371
+ return applyDOMChangeInner(view, change, newSel, lastKey);
6383
6372
  }
6384
6373
  else if (newSel && !newSel.main.eq(sel)) {
6385
6374
  let scrollIntoView = false, userEvent = "select";
@@ -6395,6 +6384,38 @@ function applyDOMChange(view, domChange) {
6395
6384
  return false;
6396
6385
  }
6397
6386
  }
6387
+ function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
6388
+ if (browser.ios && view.inputState.flushIOSKey(change))
6389
+ return true;
6390
+ let sel = view.state.selection.main;
6391
+ // Android browsers don't fire reasonable key events for enter,
6392
+ // backspace, or delete. So this detects changes that look like
6393
+ // they're caused by those keys, and reinterprets them as key
6394
+ // events. (Some of these keys are also handled by beforeinput
6395
+ // events and the pendingAndroidKey mechanism, but that's not
6396
+ // reliable in all situations.)
6397
+ if (browser.android &&
6398
+ ((change.to == sel.to &&
6399
+ // GBoard will sometimes remove a space it just inserted
6400
+ // after a completion when you press enter
6401
+ (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6402
+ change.insert.length == 1 && change.insert.lines == 2 &&
6403
+ dispatchKey(view.contentDOM, "Enter", 13)) ||
6404
+ ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6405
+ lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6406
+ dispatchKey(view.contentDOM, "Backspace", 8)) ||
6407
+ (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6408
+ dispatchKey(view.contentDOM, "Delete", 46))))
6409
+ return true;
6410
+ let text = change.insert.toString();
6411
+ if (view.inputState.composing >= 0)
6412
+ view.inputState.composing++;
6413
+ let defaultTr;
6414
+ let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6415
+ if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6416
+ view.dispatch(defaultInsert());
6417
+ return true;
6418
+ }
6398
6419
  function applyDefaultInsert(view, change, newSel) {
6399
6420
  let tr, startState = view.state, sel = startState.selection.main;
6400
6421
  if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
@@ -6521,6 +6542,7 @@ class DOMObserver {
6521
6542
  constructor(view) {
6522
6543
  this.view = view;
6523
6544
  this.active = false;
6545
+ this.editContext = null;
6524
6546
  // The known selection. Kept in our own object, as opposed to just
6525
6547
  // directly accessing the selection because:
6526
6548
  // - Safari doesn't report the right selection in shadow DOM
@@ -6565,6 +6587,10 @@ class DOMObserver {
6565
6587
  else
6566
6588
  this.flush();
6567
6589
  });
6590
+ if (window.EditContext) {
6591
+ this.editContext = new EditContextManager(view);
6592
+ view.contentDOM.editContext = this.editContext.editContext;
6593
+ }
6568
6594
  if (useCharData)
6569
6595
  this.onCharData = (event) => {
6570
6596
  this.queue.push({ target: event.target,
@@ -6615,6 +6641,8 @@ class DOMObserver {
6615
6641
  onScroll(e) {
6616
6642
  if (this.intersecting)
6617
6643
  this.flush(false);
6644
+ if (this.editContext)
6645
+ this.view.requestMeasure(this.editContext.measureReq);
6618
6646
  this.onScrollChanged(e);
6619
6647
  }
6620
6648
  onResize() {
@@ -6923,6 +6951,10 @@ class DOMObserver {
6923
6951
  win.removeEventListener("beforeprint", this.onPrint);
6924
6952
  win.document.removeEventListener("selectionchange", this.onSelectionChange);
6925
6953
  }
6954
+ update(update) {
6955
+ if (this.editContext)
6956
+ this.editContext.update(update);
6957
+ }
6926
6958
  destroy() {
6927
6959
  var _a, _b, _c;
6928
6960
  this.stop();
@@ -6982,6 +7014,161 @@ function safariSelectionRangeHack(view, selection) {
6982
7014
  view.contentDOM.removeEventListener("beforeinput", read, true);
6983
7015
  return found ? buildSelectionRangeFromRange(view, found) : null;
6984
7016
  }
7017
+ class EditContextManager {
7018
+ constructor(view) {
7019
+ // The document window for which the text in the context is
7020
+ // maintained. For large documents, this may be smaller than the
7021
+ // editor document. This window always includes the selection head.
7022
+ this.from = 0;
7023
+ this.to = 0;
7024
+ // When applying a transaction, this is used to compare the change
7025
+ // made to the context content to the change in the transaction in
7026
+ // order to make the minimal changes to the context (since touching
7027
+ // that sometimes breaks series of multiple edits made for a single
7028
+ // user action on some Android keyboards)
7029
+ this.pendingContextChange = null;
7030
+ this.resetRange(view.state);
7031
+ let context = this.editContext = new window.EditContext({
7032
+ text: view.state.doc.sliceString(this.from, this.to),
7033
+ selectionStart: this.toContextPos(Math.max(this.from, Math.min(this.to, view.state.selection.main.anchor))),
7034
+ selectionEnd: this.toContextPos(view.state.selection.main.head)
7035
+ });
7036
+ context.addEventListener("textupdate", e => {
7037
+ let { anchor } = view.state.selection.main;
7038
+ let change = { from: this.toEditorPos(e.updateRangeStart),
7039
+ to: this.toEditorPos(e.updateRangeEnd),
7040
+ insert: Text.of(e.text.split("\n")) };
7041
+ // If the window doesn't include the anchor, assume changes
7042
+ // adjacent to a side go up to the anchor.
7043
+ if (change.from == this.from && anchor < this.from)
7044
+ change.from = anchor;
7045
+ else if (change.to == this.to && anchor > this.to)
7046
+ change.to = anchor;
7047
+ // Edit context sometimes fire empty changes
7048
+ if (change.from == change.to && !change.insert.length)
7049
+ return;
7050
+ this.pendingContextChange = change;
7051
+ applyDOMChangeInner(view, change, EditorSelection.single(this.toEditorPos(e.selectionStart), this.toEditorPos(e.selectionEnd)));
7052
+ // If the transaction didn't flush our change, revert it so
7053
+ // that the context is in sync with the editor state again.
7054
+ if (this.pendingContextChange)
7055
+ this.revertPending(view.state);
7056
+ });
7057
+ context.addEventListener("characterboundsupdate", e => {
7058
+ let rects = [], prev = null;
7059
+ for (let i = this.toEditorPos(e.rangeStart), end = this.toEditorPos(e.rangeEnd); i < end; i++) {
7060
+ let rect = view.coordsForChar(i);
7061
+ prev = (rect && new DOMRect(rect.left, rect.right, rect.right - rect.left, rect.bottom - rect.top))
7062
+ || prev || new DOMRect;
7063
+ rects.push(prev);
7064
+ }
7065
+ context.updateCharacterBounds(e.rangeStart, rects);
7066
+ });
7067
+ context.addEventListener("textformatupdate", e => {
7068
+ let deco = [];
7069
+ for (let format of e.getTextFormats()) {
7070
+ let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
7071
+ if (lineStyle != "None" && thickness != "None") {
7072
+ let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
7073
+ deco.push(Decoration.mark({ attributes: { style } })
7074
+ .range(this.toEditorPos(format.rangeStart), this.toEditorPos(format.rangeEnd)));
7075
+ }
7076
+ }
7077
+ view.dispatch({ effects: setEditContextFormatting.of(Decoration.set(deco)) });
7078
+ });
7079
+ context.addEventListener("compositionstart", () => {
7080
+ if (view.inputState.composing < 0) {
7081
+ view.inputState.composing = 0;
7082
+ view.inputState.compositionFirstChange = true;
7083
+ }
7084
+ });
7085
+ context.addEventListener("compositionend", () => {
7086
+ view.inputState.composing = -1;
7087
+ view.inputState.compositionFirstChange = null;
7088
+ });
7089
+ this.measureReq = { read: view => {
7090
+ this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
7091
+ let sel = getSelection(view.root);
7092
+ if (sel && sel.rangeCount)
7093
+ this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
7094
+ } };
7095
+ }
7096
+ applyEdits(update) {
7097
+ let off = 0, abort = false, pending = this.pendingContextChange;
7098
+ update.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
7099
+ if (abort)
7100
+ return;
7101
+ let dLen = insert.length - (toA - fromA);
7102
+ if (pending && toA >= pending.to) {
7103
+ if (pending.from == fromA && pending.to == toA && pending.insert.eq(insert)) {
7104
+ pending = this.pendingContextChange = null; // Match
7105
+ off += dLen;
7106
+ return;
7107
+ }
7108
+ else { // Mismatch, revert
7109
+ pending = null;
7110
+ this.revertPending(update.state);
7111
+ }
7112
+ }
7113
+ fromA += off;
7114
+ toA += off;
7115
+ if (toA <= this.from) { // Before the window
7116
+ this.from += dLen;
7117
+ this.to += dLen;
7118
+ }
7119
+ else if (fromA < this.to) { // Overlaps with window
7120
+ if (fromA < this.from || toA > this.to || (this.to - this.from) + insert.length > 30000 /* CxVp.MaxSize */) {
7121
+ abort = true;
7122
+ return;
7123
+ }
7124
+ this.editContext.updateText(this.toContextPos(fromA), this.toContextPos(toA), insert.toString());
7125
+ this.to += dLen;
7126
+ }
7127
+ off += dLen;
7128
+ });
7129
+ if (pending && !abort)
7130
+ this.revertPending(update.state);
7131
+ return !abort;
7132
+ }
7133
+ update(update) {
7134
+ if (!this.applyEdits(update) || !this.rangeIsValid(update.state)) {
7135
+ this.pendingContextChange = null;
7136
+ this.resetRange(update.state);
7137
+ this.editContext.updateText(0, this.editContext.text.length, update.state.doc.sliceString(this.from, this.to));
7138
+ this.setSelection(update.state);
7139
+ }
7140
+ else if (update.docChanged || update.selectionSet) {
7141
+ this.setSelection(update.state);
7142
+ }
7143
+ if (update.geometryChanged || update.docChanged || update.selectionSet)
7144
+ update.view.requestMeasure(this.measureReq);
7145
+ }
7146
+ resetRange(state) {
7147
+ let { head } = state.selection.main;
7148
+ this.from = Math.max(0, head - 10000 /* CxVp.Margin */);
7149
+ this.to = Math.min(state.doc.length, head + 10000 /* CxVp.Margin */);
7150
+ }
7151
+ revertPending(state) {
7152
+ let pending = this.pendingContextChange;
7153
+ this.pendingContextChange = null;
7154
+ this.editContext.updateText(this.toContextPos(pending.from), this.toContextPos(pending.to + pending.insert.length), state.doc.sliceString(pending.from, pending.to));
7155
+ }
7156
+ setSelection(state) {
7157
+ let { main } = state.selection;
7158
+ let start = this.toContextPos(Math.max(this.from, Math.min(this.to, main.anchor)));
7159
+ let end = this.toContextPos(main.head);
7160
+ if (this.editContext.selectionStart != start || this.editContext.selectionEnd != end)
7161
+ this.editContext.updateSelection(start, end);
7162
+ }
7163
+ rangeIsValid(state) {
7164
+ let { head } = state.selection.main;
7165
+ return !(this.from > 0 && head - this.from < 500 /* CxVp.MinMargin */ ||
7166
+ this.to < state.doc.length && this.to - head < 500 /* CxVp.MinMargin */ ||
7167
+ this.to - this.from > 10000 /* CxVp.Margin */ * 3);
7168
+ }
7169
+ toEditorPos(contextPos) { return contextPos + this.from; }
7170
+ toContextPos(editorPos) { return editorPos - this.from; }
7171
+ }
6985
7172
 
6986
7173
  // The editor's update state machine looks something like this:
6987
7174
  //
@@ -8252,8 +8439,9 @@ function buildKeymap(bindings, platform = currentPlatform) {
8252
8439
  let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
8253
8440
  if (!scopeObj._any)
8254
8441
  scopeObj._any = { preventDefault: false, stopPropagation: false, run: [] };
8442
+ let { any } = b;
8255
8443
  for (let key in scopeObj)
8256
- scopeObj[key].run.push(b.any);
8444
+ scopeObj[key].run.push(view => any(view, currentKeyEvent));
8257
8445
  }
8258
8446
  let name = b[platform] || b.key;
8259
8447
  if (!name)
@@ -8266,7 +8454,9 @@ function buildKeymap(bindings, platform = currentPlatform) {
8266
8454
  }
8267
8455
  return bound;
8268
8456
  }
8457
+ let currentKeyEvent = null;
8269
8458
  function runHandlers(map, event, view, scope) {
8459
+ currentKeyEvent = event;
8270
8460
  let name = keyName(event);
8271
8461
  let charCode = codePointAt(name, 0), isChar = codePointSize(charCode) == name.length && name != " ";
8272
8462
  let prefix = "", handled = false, prevented = false, stopPropagation = false;
@@ -8283,7 +8473,7 @@ function runHandlers(map, event, view, scope) {
8283
8473
  for (let cmd of binding.run)
8284
8474
  if (!ran.has(cmd)) {
8285
8475
  ran.add(cmd);
8286
- if (cmd(view, event)) {
8476
+ if (cmd(view)) {
8287
8477
  if (binding.stopPropagation)
8288
8478
  stopPropagation = true;
8289
8479
  return true;
@@ -8325,6 +8515,7 @@ function runHandlers(map, event, view, scope) {
8325
8515
  handled = true;
8326
8516
  if (handled && stopPropagation)
8327
8517
  event.stopPropagation();
8518
+ currentKeyEvent = null;
8328
8519
  return handled;
8329
8520
  }
8330
8521
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.26.3",
3
+ "version": "6.26.4-edit-context",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",