@dxos/react-ui-editor 0.3.11-main.008b7c6 → 0.3.11-main.00a28cb

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 (92) hide show
  1. package/dist/lib/browser/index.mjs +558 -629
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +12 -5
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  11. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
  13. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  16. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/defs.d.ts +4 -4
  18. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  19. package/dist/types/src/extensions/automerge/semaphore.d.ts +4 -9
  20. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  21. package/dist/types/src/extensions/code.d.ts.map +1 -1
  22. package/dist/types/src/extensions/comments.d.ts +14 -5
  23. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  24. package/dist/types/src/{util → extensions}/cursor.d.ts +9 -1
  25. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  26. package/dist/types/src/extensions/hr.d.ts.map +1 -1
  27. package/dist/types/src/extensions/image.d.ts.map +1 -1
  28. package/dist/types/src/extensions/index.d.ts +2 -1
  29. package/dist/types/src/extensions/index.d.ts.map +1 -1
  30. package/dist/types/src/extensions/link.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  32. package/dist/types/src/extensions/outliner.d.ts +4 -0
  33. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  34. package/dist/types/src/extensions/tasklist.d.ts +5 -1
  35. package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
  36. package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
  37. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
  38. package/dist/types/src/hooks/index.d.ts +1 -1
  39. package/dist/types/src/hooks/index.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useTextModel.d.ts +0 -5
  41. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  42. package/dist/types/src/hooks/yjs.d.ts.map +1 -1
  43. package/dist/types/src/index.d.ts +3 -0
  44. package/dist/types/src/index.d.ts.map +1 -1
  45. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  46. package/dist/types/src/styles/tokens.d.ts +1 -0
  47. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  48. package/dist/types/src/themes/default.d.ts.map +1 -1
  49. package/dist/types/src/util.d.ts.map +1 -0
  50. package/package.json +25 -24
  51. package/src/components/TextEditor/MarkdownEditor.stories.tsx +41 -37
  52. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  53. package/src/components/TextEditor/TextEditor.tsx +71 -67
  54. package/src/components/TextEditor/comments.stories.tsx +355 -0
  55. package/src/extensions/autocomplete.ts +18 -17
  56. package/src/extensions/automerge/automerge.ts +12 -14
  57. package/src/extensions/automerge/cursor.ts +2 -1
  58. package/src/extensions/automerge/defs.ts +13 -9
  59. package/src/extensions/automerge/semaphore.ts +10 -12
  60. package/src/extensions/awareness.ts +27 -33
  61. package/src/extensions/code.ts +28 -23
  62. package/src/extensions/comments.ts +131 -144
  63. package/src/extensions/cursor.ts +46 -0
  64. package/src/extensions/hr.ts +10 -12
  65. package/src/extensions/image.ts +40 -22
  66. package/src/extensions/index.ts +2 -1
  67. package/src/extensions/link.ts +86 -88
  68. package/src/extensions/markdown/highlight.ts +3 -12
  69. package/src/extensions/outliner.ts +12 -0
  70. package/src/extensions/table.ts +10 -4
  71. package/src/extensions/tasklist.ts +54 -79
  72. package/src/extensions/yjs/cursor.ts +2 -1
  73. package/src/extensions/yjs/yjs.test.ts +1 -1
  74. package/src/extensions/yjs/yjs.ts +2 -2
  75. package/src/hooks/index.ts +2 -1
  76. package/src/hooks/useTextModel.ts +4 -7
  77. package/src/hooks/yjs.ts +1 -23
  78. package/src/index.ts +3 -0
  79. package/src/styles/markdown.ts +0 -2
  80. package/src/styles/tokens.ts +3 -0
  81. package/src/themes/default.ts +51 -78
  82. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  83. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  84. package/dist/types/src/util/cursor.d.ts.map +0 -1
  85. package/dist/types/src/util/index.d.ts +0 -3
  86. package/dist/types/src/util/index.d.ts.map +0 -1
  87. package/dist/types/src/util/util.d.ts.map +0 -1
  88. package/src/extensions/mermaid.ts +0 -11
  89. package/src/util/cursor.ts +0 -29
  90. package/src/util/index.ts +0 -6
  91. /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
  92. /package/src/{util/util.ts → util.ts} +0 -0
@@ -4,20 +4,16 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { Facet, StateField, type Extension, type Transaction } from '@codemirror/state';
7
+ import { StateField, type Extension, type Transaction } from '@codemirror/state';
8
8
  import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '@codemirror/view';
9
9
 
10
10
  import { next as A } from '@dxos/automerge/automerge';
11
11
  import { type Prop } from '@dxos/automerge/automerge';
12
12
 
13
13
  import { cursorConverter } from './cursor';
14
- import { effectType, type IDocHandle, isReconcileTx, type Value } from './defs';
14
+ import { effectType, type IDocHandle, isReconcileTx, semaphoreFacet, type Value } from './defs';
15
15
  import { PatchSemaphore } from './semaphore';
16
- import { CursorConverter } from '../../util';
17
-
18
- const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
19
- combine: (values) => values.at(-1)!, // Take last.
20
- });
16
+ import { Cursor } from '../cursor';
21
17
 
22
18
  export type AutomergeOptions = {
23
19
  handle: IDocHandle;
@@ -25,19 +21,20 @@ export type AutomergeOptions = {
25
21
  };
26
22
 
27
23
  export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
28
- // TODO(burdon): Rename.
29
24
  const stateField: StateField<Value> = StateField.define({
30
25
  create: () => ({
31
26
  lastHeads: A.getHeads(handle.docSync()!),
32
27
  unreconciledTransactions: [],
33
28
  path: path.slice(),
34
29
  }),
30
+
35
31
  update: (value: Value, tr: Transaction) => {
36
32
  const result: Value = {
37
33
  lastHeads: value.lastHeads,
38
34
  unreconciledTransactions: value.unreconciledTransactions.slice(),
39
35
  path: path.slice(),
40
36
  };
37
+
41
38
  let clearUnreconciled = false;
42
39
  for (const effect of tr.effects) {
43
40
  if (effect.is(effectType)) {
@@ -45,6 +42,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
45
42
  clearUnreconciled = true;
46
43
  }
47
44
  }
45
+
48
46
  if (clearUnreconciled) {
49
47
  result.unreconciledTransactions = [];
50
48
  } else {
@@ -52,23 +50,23 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
52
50
  result.unreconciledTransactions.push(tr);
53
51
  }
54
52
  }
53
+
55
54
  return result;
56
55
  },
57
56
  });
58
57
 
59
- const semaphore = new PatchSemaphore(stateField);
58
+ const semaphore = new PatchSemaphore(handle, stateField);
60
59
 
61
60
  const viewPlugin = ViewPlugin.fromClass(
62
- class AutomergeCodemirrorViewPlugin implements PluginValue {
61
+ class AutomergeViewPlugin implements PluginValue {
63
62
  constructor(private readonly _view: EditorView) {
64
63
  handle.addListener('change', this._handleChange);
65
64
  }
66
65
 
67
66
  update(update: ViewUpdate) {
68
67
  if (update.transactions.length > 0 && update.transactions.some((tr) => !isReconcileTx(tr))) {
69
- // TODO(burdon): This is a hack to ensure that the view is updated after the transaction is applied.
70
68
  queueMicrotask(() => {
71
- this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
69
+ this._view.state.facet(semaphoreFacet).reconcile(this._view);
72
70
  });
73
71
  }
74
72
  }
@@ -78,14 +76,14 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
78
76
  }
79
77
 
80
78
  private _handleChange = () => {
81
- this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
79
+ this._view.state.facet(semaphoreFacet).reconcile(this._view);
82
80
  };
83
81
  },
84
82
  );
85
83
 
86
84
  return {
87
85
  extension: [
88
- CursorConverter.of(cursorConverter(handle, path)),
86
+ Cursor.converter.of(cursorConverter(handle, path)),
89
87
  semaphoreFacet.of(semaphore),
90
88
  stateField,
91
89
  viewPlugin,
@@ -8,7 +8,7 @@ import type { Prop } from '@dxos/automerge/automerge';
8
8
  import { next as automerge } from '@dxos/automerge/automerge';
9
9
 
10
10
  import { type IDocHandle } from './defs';
11
- import { type CursorConverter } from '../../util';
11
+ import { type CursorConverter } from '../cursor';
12
12
 
13
13
  export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConverter => ({
14
14
  // TODO(burdon): Handle assoc to associate with a previous character.
@@ -26,6 +26,7 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
26
26
  // NOTE: Slice is needed because getCursor mutates the array.
27
27
  return automerge.getCursor(doc, path.slice(), pos);
28
28
  },
29
+
29
30
  fromCursor: (cursor) => {
30
31
  if (cursor === '') {
31
32
  return 0;
@@ -1,26 +1,22 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
+ // Copyright 2024 Automerge
3
4
  // Ref: https://github.com/automerge/automerge-codemirror
4
5
  //
5
6
 
6
- import {
7
- Annotation,
8
- StateEffect,
9
- type StateField,
10
- type EditorState,
11
- type Transaction,
12
- type TransactionSpec,
13
- } from '@codemirror/state';
7
+ import { Annotation, StateEffect, type StateField, type EditorState, type Transaction, Facet } from '@codemirror/state';
14
8
 
15
9
  import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
16
10
 
11
+ import { type PatchSemaphore } from './semaphore';
12
+
17
13
  export type Value = {
18
14
  lastHeads: Heads;
19
15
  path: Prop[];
20
16
  unreconciledTransactions: Transaction[];
21
17
  };
22
18
 
23
- type UpdateHeads = {
19
+ export type UpdateHeads = {
24
20
  newHeads: Heads;
25
21
  };
26
22
 
@@ -36,6 +32,12 @@ export const reconcileAnnotationType = Annotation.define<unknown>();
36
32
 
37
33
  export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
38
34
 
35
+ // TODO(burdon): Rename sync/create generic def (like cursor) to decouple automerge deps.
36
+ export const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
37
+ combine: (values) => values.at(-1)!, // Take last.
38
+ });
39
+
40
+ /*
39
41
  export const makeReconcile = (tr: TransactionSpec) => {
40
42
  if (tr.annotations != null) {
41
43
  if (tr.annotations instanceof Array) {
@@ -46,11 +48,13 @@ export const makeReconcile = (tr: TransactionSpec) => {
46
48
  } else {
47
49
  tr.annotations = [reconcileAnnotationType.of({})];
48
50
  }
51
+
49
52
  // return {
50
53
  // ...tr,
51
54
  // annotations: reconcileAnnotationType.of({})
52
55
  // }
53
56
  };
57
+ */
54
58
 
55
59
  export type IDocHandle<T = any> = {
56
60
  docSync(): Doc<T> | undefined;
@@ -21,27 +21,23 @@ import {
21
21
  import { updateAutomerge } from './update-automerge';
22
22
  import { updateCodeMirror } from './update-codemirror';
23
23
 
24
- type Doc<T> = automerge.Doc<T>;
25
- type Heads = automerge.Heads;
26
-
27
- type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
28
-
29
24
  /**
30
- * TODO(burdon): Comment.
25
+ * Implements three-way merge (on each mutation/key-stroke).
31
26
  */
32
27
  export class PatchSemaphore {
33
28
  _inReconcile = false;
34
- _queue: Array<ChangeFn> = [];
35
29
 
36
- constructor(private readonly _field: StateField<Value>) {}
30
+ constructor(private readonly _handle: IDocHandle, private readonly _field: StateField<Value>) {}
37
31
 
38
- reconcile = (handle: IDocHandle, view: EditorView) => {
32
+ reconcile = (view: EditorView) => {
39
33
  if (this._inReconcile) {
40
34
  return;
41
35
  }
42
36
  this._inReconcile = true;
43
37
 
44
38
  const path = getPath(view.state, this._field);
39
+
40
+ // Get the heads before the unreconciled transactions are applied.
45
41
  const oldHeads = getLastHeads(view.state, this._field);
46
42
  let selection = view.state.selection;
47
43
 
@@ -58,16 +54,18 @@ export class PatchSemaphore {
58
54
  }
59
55
 
60
56
  // Apply the unreconciled transactions to the document.
61
- let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
57
+ let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
62
58
 
63
59
  // NOTE: null and undefined each come from automerge and repo respectively.
64
60
  if (newHeads === null || newHeads === undefined) {
65
61
  // TODO: @alexjg this is the call that's resetting the editor state on click
66
- newHeads = automerge.getHeads(handle.docSync()!);
62
+ newHeads = automerge.getHeads(this._handle.docSync()!);
67
63
  }
68
64
 
69
65
  // Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
70
- const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.docSync()!, oldHeads, newHeads);
66
+ const diff = automerge.equals(oldHeads, newHeads)
67
+ ? []
68
+ : automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
71
69
  updateCodeMirror(view, selection, path, diff);
72
70
 
73
71
  view.dispatch({
@@ -16,7 +16,7 @@ import {
16
16
  import { Event } from '@dxos/async';
17
17
  import { Context } from '@dxos/context';
18
18
 
19
- import { CursorConverter } from '../util';
19
+ import { Cursor, type CursorConverter } from './cursor';
20
20
 
21
21
  export interface AwarenessProvider {
22
22
  remoteStateChange: Event<void>;
@@ -91,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
91
91
  private _lastHead?: number = undefined;
92
92
 
93
93
  constructor(view: EditorView) {
94
- this._cursorConverter = view.state.facet(CursorConverter);
94
+ this._cursorConverter = view.state.facet(Cursor.converter);
95
95
  this._provider = view.state.facet(AwarenessProvider);
96
96
  this._provider.open();
97
97
  this._provider.remoteStateChange.on(this._ctx, () => {
@@ -156,7 +156,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
156
156
  to: end,
157
157
  value: Decoration.mark({
158
158
  attributes: { style: `background-color: ${lightColor}` },
159
- class: 'cm-ySelection',
159
+ class: 'cm-collab-selection',
160
160
  }),
161
161
  });
162
162
  } else {
@@ -166,7 +166,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
166
166
  to: startLine.from + startLine.length,
167
167
  value: Decoration.mark({
168
168
  attributes: { style: `background-color: ${color}` },
169
- class: 'cm-ySelection',
169
+ class: 'cm-collab-selection',
170
170
  }),
171
171
  });
172
172
 
@@ -176,7 +176,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
176
176
  to: end,
177
177
  value: Decoration.mark({
178
178
  attributes: { style: `background-color: ${color}` },
179
- class: 'cm-ySelection',
179
+ class: 'cm-collab-selection',
180
180
  }),
181
181
  });
182
182
 
@@ -186,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
186
186
  from: linePos,
187
187
  to: linePos,
188
188
  value: Decoration.line({
189
- attributes: { style: `background-color: ${color}`, class: 'cm-yLineSelection' },
189
+ attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
190
190
  }),
191
191
  });
192
192
  }
@@ -208,24 +208,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
208
208
  }
209
209
 
210
210
  class RemoteCaretWidget extends WidgetType {
211
- constructor(public name: string, public color: string) {
211
+ constructor(private readonly _name: string, private readonly _color: string) {
212
212
  super();
213
- this.name = name;
214
- this.color = color;
215
213
  }
216
214
 
217
- toDOM(): HTMLElement {
215
+ override toDOM(): HTMLElement {
218
216
  const span = document.createElement('span');
219
- span.className = 'cm-ySelectionCaret';
220
- span.style.backgroundColor = this.color;
221
- span.style.borderColor = this.color;
217
+ span.className = 'cm-collab-selectionCaret';
218
+ span.style.backgroundColor = this._color;
219
+ span.style.borderColor = this._color;
222
220
 
223
221
  const dot = document.createElement('div');
224
- dot.className = 'cm-ySelectionCaretDot';
222
+ dot.className = 'cm-collab-selectionCaretDot';
225
223
 
226
224
  const info = document.createElement('div');
227
- info.className = 'cm-ySelectionInfo';
228
- info.innerText = this.name;
225
+ info.className = 'cm-collab-selectionInfo';
226
+ info.innerText = this._name;
229
227
 
230
228
  span.appendChild(document.createTextNode('\u2060'));
231
229
  span.appendChild(dot);
@@ -236,18 +234,14 @@ class RemoteCaretWidget extends WidgetType {
236
234
  return span;
237
235
  }
238
236
 
239
- override eq(widget: this) {
240
- return widget.color === this.color;
241
- }
242
-
243
- compare(widget: this) {
244
- return widget.color === this.color;
245
- }
246
-
247
237
  override updateDOM() {
248
238
  return false;
249
239
  }
250
240
 
241
+ override eq(widget: this) {
242
+ return widget._color === this._color;
243
+ }
244
+
251
245
  override get estimatedHeight() {
252
246
  return -1;
253
247
  }
@@ -257,14 +251,14 @@ class RemoteCaretWidget extends WidgetType {
257
251
  }
258
252
  }
259
253
 
260
- // TODO(burdon): Rename prefix (y for yjs?)
254
+ // TODO(burdon): Mixed case?
261
255
  const styles = EditorView.baseTheme({
262
- '.cm-ySelection': {},
263
- '.cm-yLineSelection': {
256
+ '.cm-collab-selection': {},
257
+ '.cm-collab-selectionLine': {
264
258
  padding: 0,
265
259
  margin: '0px 2px 0px 4px',
266
260
  },
267
- '.cm-ySelectionCaret': {
261
+ '.cm-collab-selectionCaret': {
268
262
  position: 'relative',
269
263
  borderLeft: '1px solid black',
270
264
  borderRight: '1px solid black',
@@ -273,7 +267,7 @@ const styles = EditorView.baseTheme({
273
267
  boxSizing: 'border-box',
274
268
  display: 'inline',
275
269
  },
276
- '.cm-ySelectionCaretDot': {
270
+ '.cm-collab-selectionCaretDot': {
277
271
  borderRadius: '50%',
278
272
  position: 'absolute',
279
273
  width: '.4em',
@@ -284,11 +278,11 @@ const styles = EditorView.baseTheme({
284
278
  transition: 'transform .3s ease-in-out',
285
279
  boxSizing: 'border-box',
286
280
  },
287
- '.cm-ySelectionCaret:hover > .cm-ySelectionCaretDot': {
281
+ '.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
288
282
  transformOrigin: 'bottom center',
289
283
  transform: 'scale(0)',
290
284
  },
291
- '.cm-ySelectionInfo': {
285
+ '.cm-collab-selectionInfo': {
292
286
  position: 'absolute',
293
287
  top: '-1.05em',
294
288
  left: '-1px',
@@ -304,12 +298,12 @@ const styles = EditorView.baseTheme({
304
298
  zIndex: 101,
305
299
  transition: 'opacity .3s ease-in-out',
306
300
  backgroundColor: 'inherit',
307
- // these should be separate
301
+ // These should be separate.
308
302
  opacity: 0,
309
303
  transitionDelay: '0s',
310
304
  whiteSpace: 'nowrap',
311
305
  },
312
- '.cm-ySelectionCaret:hover > .cm-ySelectionInfo': {
306
+ '.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
313
307
  opacity: 1,
314
308
  transitionDelay: '0s',
315
309
  },
@@ -11,24 +11,23 @@ import {
11
11
  Decoration,
12
12
  type BlockInfo,
13
13
  } from '@codemirror/view';
14
- import get from 'lodash.get';
15
14
 
16
15
  import { mx } from '@dxos/react-ui-theme';
17
16
 
18
- import { tokens } from '../styles';
17
+ import { getToken } from '../styles';
19
18
 
20
19
  const CODE_REGEX = /```[\s\S]*?```/gs;
21
20
 
22
21
  // TODO(burdon): Reconcile with theme.
23
22
  const styles = EditorView.baseTheme({
24
23
  '& .cm-codeblock': {
25
- fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
24
+ fontFamily: getToken('fontFamily.mono', []).join(','),
26
25
  },
27
26
  '&light [aria-readonly="true"] .cm-codeblock': {
28
- background: get(tokens, 'extend.colors.neutral.50'),
27
+ background: getToken('extend.colors.neutral.50'),
29
28
  },
30
29
  '&dark [aria-readonly="true"] .cm-codeblock': {
31
- background: get(tokens, 'extend.colors.neutral.850'),
30
+ background: getToken('extend.colors.neutral.850'),
32
31
  },
33
32
  '& [aria-readonly="true"] .cm-codeblock': {
34
33
  display: 'block',
@@ -54,22 +53,20 @@ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
54
53
  };
55
54
 
56
55
  export const code = () => {
57
- const getDecorations = (view: EditorView) => {
56
+ const buildDecorations = (view: EditorView) => {
58
57
  const decorations: Range<Decoration>[] = [];
59
- if (view.state.readOnly) {
60
- const text = view.state.doc.sliceString(0);
61
- const matches = text.matchAll(CODE_REGEX);
62
- const blocks = view.viewportLineBlocks;
63
- for (const match of matches) {
64
- const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
65
- for (let i = range[0]; i <= range[1]; i++) {
66
- const block = blocks[i];
67
- decorations.push(
68
- Decoration.line({
69
- class: mx('cm-codeblock', i === range[0] && 'cm-codeblock-first', i === range[1] && 'cm-codeblock-last'),
70
- }).range(block.from),
71
- );
72
- }
58
+ const text = view.state.doc.sliceString(0);
59
+ const matches = text.matchAll(CODE_REGEX);
60
+ const blocks = view.viewportLineBlocks;
61
+ for (const match of matches) {
62
+ const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
63
+ for (let i = range[0]; i <= range[1]; i++) {
64
+ const block = blocks[i];
65
+ decorations.push(
66
+ Decoration.line({
67
+ class: mx('cm-codeblock', i === range[0] && 'cm-codeblock-first', i === range[1] && 'cm-codeblock-last'),
68
+ }).range(block.from),
69
+ );
73
70
  }
74
71
  }
75
72
 
@@ -79,14 +76,22 @@ export const code = () => {
79
76
  return [
80
77
  ViewPlugin.fromClass(
81
78
  class {
79
+ hasFocus = false;
82
80
  decorations: DecorationSet;
83
81
  constructor(view: EditorView) {
84
- this.decorations = getDecorations(view);
82
+ this.decorations = buildDecorations(view);
85
83
  }
86
84
 
87
85
  update(update: ViewUpdate) {
88
- if (update.docChanged || update.viewportChanged) {
89
- this.decorations = getDecorations(update.view);
86
+ if (
87
+ // TODO(burdon): Generalize for other extensions.
88
+ this.hasFocus !== update.view.hasFocus ||
89
+ update.startState.readOnly !== update.view.state.readOnly ||
90
+ update.docChanged ||
91
+ update.viewportChanged
92
+ ) {
93
+ this.hasFocus = update.view.hasFocus;
94
+ this.decorations = buildDecorations(update.view);
90
95
  }
91
96
  }
92
97
  },