@adecore/editor-react 0.39.0 → 0.40.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.
Files changed (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
package/src/PeekPanel.tsx CHANGED
@@ -18,6 +18,7 @@ import type { PeekView } from './popups.ts';
18
18
  import { useEditorRendering } from './rendering-context.ts';
19
19
 
20
20
  const TAB_SIZE = 4;
21
+
21
22
  const CODE =
22
23
  'font-mono text-(length:--code-font-size) [tab-size:4] [&_.line]:block [&_.line]:h-(--code-line-height) [&_.line]:leading-(--code-line-height) [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-0 [&_pre]:whitespace-pre [&_code]:font-mono';
23
24
 
@@ -33,10 +34,13 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
33
34
  if (preview === null || highlight === undefined) {
34
35
  return;
35
36
  }
37
+
36
38
  let alive = true;
39
+
37
40
  highlight(preview.text, shikiLanguageOfPath(path ?? preview.uri), theme)
38
41
  .then((result) => alive && setHtml({ key, html: result }))
39
42
  .catch(() => undefined);
43
+
40
44
  return () => {
41
45
  alive = false;
42
46
  };
@@ -49,8 +53,10 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
49
53
  if (preview === null) {
50
54
  return <div className="min-w-0 flex-1" />;
51
55
  }
56
+
52
57
  const lines = preview.text.split('\n');
53
58
  const name = preview.name === null ? null : columnsOf(lines[preview.active] ?? '', preview.name);
59
+
54
60
  return (
55
61
  <div ref={scroller} className="relative min-w-0 flex-1 overflow-auto pt-1">
56
62
  <div className="relative flex min-w-max">
@@ -94,6 +100,7 @@ export function Preview({ preview }: { preview: PeekView['preview'] }) {
94
100
  /* The name's stretch of a line in monospace columns, which is what the code is laid out in. */
95
101
  function columnsOf(line: string, name: NameRange): NameRange {
96
102
  const start = visualColumnOf(line, name.start, TAB_SIZE);
103
+
97
104
  return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
98
105
  }
99
106
 
@@ -106,10 +113,13 @@ function PlaceText({ place }: { place: PeekPlace }) {
106
113
  if (place.text === '') {
107
114
  return <span className="min-w-0 truncate text-text-muted">{`:${place.line + 1}`}</span>;
108
115
  }
116
+
109
117
  const { name } = place;
118
+
110
119
  if (name === null) {
111
120
  return <span className="min-w-0 truncate text-text-muted">{place.text}</span>;
112
121
  }
122
+
113
123
  return (
114
124
  <span className="min-w-0 truncate text-text-muted">
115
125
  {place.text.slice(0, name.start)}
@@ -134,6 +144,7 @@ function Places({ language, view }: { language: EditorLanguage; view: PeekView }
134
144
  <div className="w-1/3 shrink-0 overflow-y-auto border-l border-border py-1">
135
145
  {view.files.map((file, fileIndex) => {
136
146
  const collapsed = view.collapsed.includes(file.uri);
147
+
137
148
  return (
138
149
  <div key={file.uri}>
139
150
  <Tooltip label={paths[fileIndex]!} side="left">
@@ -183,9 +194,11 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
183
194
  const folder = language.project.folder;
184
195
  const shown = path === null ? '' : folderOf(relativeTo(path, folder));
185
196
  const body = usePanelBody(language);
197
+
186
198
  if (view.container === null) {
187
199
  return null;
188
200
  }
201
+
189
202
  return createPortal(
190
203
  <div
191
204
  className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
package/src/PickPopup.tsx CHANGED
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
3
3
  import type { EditorRect } from '@adecore/editor';
4
4
  import type { EditorLanguage } from './editor-language.ts';
5
5
  import { PathText } from './PathText.tsx';
6
- import { keepFocusOutOfList } from './popup-focus.ts';
6
+ import { keepFocusOutOfList, useInputPopup } from './popup-focus.ts';
7
7
  import { placePopup } from './popup-placement.ts';
8
8
  import type { PickPreview, PickView } from './popups.ts';
9
9
 
@@ -47,15 +47,18 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
47
47
  // Every render: the character moves with the scroll, and the list changes size with its preview.
48
48
  useLayoutEffect(() => {
49
49
  const element = card.current;
50
+
50
51
  if (element === null) {
51
52
  return;
52
53
  }
54
+
53
55
  const placed = placePopup(
54
56
  rect,
55
57
  { width: element.offsetWidth, height: element.offsetHeight },
56
58
  { width: window.innerWidth, height: window.innerHeight },
57
59
  { gap: 2 }
58
60
  );
61
+
59
62
  element.style.left = `${Math.round(placed.left)}px`;
60
63
  element.style.top = `${Math.round(placed.top)}px`;
61
64
  element.style.maxHeight = `${Math.floor(placed.maxHeight)}px`;
@@ -67,18 +70,16 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
67
70
  }, [view.active]);
68
71
 
69
72
  const optionIds = new Map<string, string>();
73
+
70
74
  for (const group of view.groups) {
71
75
  for (const row of group.rows) {
72
76
  optionIds.set(row.id, `${listId}-option-${optionIds.size}`);
73
77
  }
74
78
  }
79
+
75
80
  const activeId = optionIds.get(view.active);
76
81
 
77
- // The focus stays in the text, so the input is what names the list and its active option.
78
- useLayoutEffect(() => {
79
- language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
80
- return () => language.editor.setInputPopup(null);
81
- }, [language, listId, activeId]);
82
+ useInputPopup(language, listId, activeId);
82
83
 
83
84
  return createPortal(
84
85
  <div
@@ -25,6 +25,7 @@ function staysInside(target: EventTarget | null): boolean {
25
25
  */
26
26
  function Suggestions({ names, active, onPick, onFocusBack }: { names: readonly string[]; active: number; onPick(name: string): void; onFocusBack(): void }) {
27
27
  const { t } = useTranslation('editor');
28
+
28
29
  return (
29
30
  <div className="flex w-[250px] flex-col p-1.25" {...keepFocusOutOfList(onFocusBack)}>
30
31
  <div className="flex items-center gap-1.5 px-2.25 pt-1.25 pb-1 text-xs font-semibold text-text-faint">
@@ -54,6 +55,7 @@ function Suggestions({ names, active, onPick, onFocusBack }: { names: readonly s
54
55
  function Hints({ view, active, onPick, onFocusBack }: { view: RenameView; active: number; onPick(name: string): void; onFocusBack(): void }) {
55
56
  const { t } = useTranslation('editor');
56
57
  const { occurrences } = view;
58
+
57
59
  return (
58
60
  <div className="flex flex-col">
59
61
  {view.error !== null && (
@@ -88,16 +90,20 @@ function Preview({ language, view }: { language: EditorLanguage; view: RenameVie
88
90
  const { t } = useTranslation('editor');
89
91
  const folder = language.project.folder;
90
92
  const operations = view.operations ?? [];
93
+
91
94
  const shownPath = (uri: string): string => {
92
95
  const path = fileUriToPath(uri);
96
+
93
97
  return path === null ? uri : relativeTo(path, folder);
94
98
  };
99
+
95
100
  return (
96
101
  <div className="flex w-[520px] max-w-full flex-col" onPointerDown={(event) => event.preventDefault()}>
97
102
  <div className="max-h-[320px] overflow-y-auto">
98
103
  {view.files.map((file) => {
99
104
  const path = fileUriToPath(file.uri);
100
105
  const shown = path === null ? file.uri : relativeTo(path, folder);
106
+
101
107
  return (
102
108
  <div key={file.uri} className="border-b border-border last:border-b-0">
103
109
  <div className="flex items-center gap-2 px-3 py-1.5 text-xs">
@@ -170,19 +176,24 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
170
176
 
171
177
  function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
172
178
  const names = view.phase === 'input' ? view.suggestions : [];
179
+
173
180
  if (names.length > 0 && !event.nativeEvent.isComposing && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
174
181
  const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
182
+
175
183
  if (step !== 0 || event.key === 'Tab') {
176
184
  event.preventDefault();
177
185
  event.stopPropagation();
186
+
178
187
  if (event.key === 'Tab') {
179
188
  fill(names[Math.min(active, names.length - 1)]!);
180
189
  } else {
181
190
  setActive((active + step + names.length) % names.length);
182
191
  }
192
+
183
193
  return;
184
194
  }
185
195
  }
196
+
186
197
  if (event.key === 'Escape') {
187
198
  event.preventDefault();
188
199
  event.stopPropagation();
@@ -190,6 +201,7 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
190
201
  } else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
191
202
  event.preventDefault();
192
203
  event.stopPropagation();
204
+
193
205
  if (view.phase === 'preview') {
194
206
  void language.rename.confirm();
195
207
  } else if (!view.busy) {
@@ -199,6 +211,7 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
199
211
  }
200
212
 
201
213
  const symbolWidth = endRect === null ? 0 : Math.max(0, endRect.left - rect.left);
214
+
202
215
  return createPortal(
203
216
  <>
204
217
  <input
@@ -10,6 +10,7 @@ import { shikiLanguageOf } from './language-ids.ts';
10
10
  import { Markdown } from './Markdown.tsx';
11
11
 
12
12
  const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
13
+
13
14
  const KNOWN = new Set([
14
15
  'deprecated',
15
16
  'template',
@@ -35,6 +36,7 @@ function Code({ children, faint = false }: { children: string; faint?: boolean }
35
36
  /* Markdown prose and fenced code as the hover draws them: the blocks in the code face, the rest muted. */
36
37
  function Prose({ text, language, linkable, compact = false }: { text: string; language: string; linkable: boolean; compact?: boolean }) {
37
38
  const plain = linkable ? text : text.replace(LINK, '$1');
39
+
38
40
  return (
39
41
  <>
40
42
  {markdownParts(plain).map((part, index) =>
@@ -55,8 +57,10 @@ function Prose({ text, language, linkable, compact = false }: { text: string; la
55
57
  /* A value of a tag, in the second column of the table; `spaced` opens a new section with more room above. */
56
58
  function TagRow({ tag, language, linkable, spaced }: { tag: DocTagEntry; language: string; linkable: boolean; spaced: boolean }) {
57
59
  const target = tag.name === 'see' && tag.subject !== null ? tag.subject : null;
60
+
58
61
  const link =
59
62
  target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
63
+
60
64
  return (
61
65
  <dd className={`col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`}>
62
66
  {tag.name === 'example' ? (
@@ -102,15 +106,19 @@ function DocBlock({ language, comment, parsed, linkable }: DocBlockProps) {
102
106
  // One handler for every link, since the links are made by the Markdown renderer and not by us.
103
107
  const follow = (event: MouseEvent<HTMLDivElement>): void => {
104
108
  const anchor = (event.target as HTMLElement).closest<HTMLAnchorElement>('a');
109
+
105
110
  if (anchor === null) {
106
111
  return;
107
112
  }
113
+
108
114
  event.preventDefault();
109
115
  const mod = language.apple ? event.metaKey : event.ctrlKey;
110
116
  const href = anchor.getAttribute('href') ?? '';
117
+
111
118
  if (!mod) {
112
119
  return;
113
120
  }
121
+
114
122
  if (href.startsWith(DOC_LINK_PREFIX)) {
115
123
  feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), comment);
116
124
  } else if (/^(https?:|mailto:)/i.test(href)) {
@@ -159,9 +167,12 @@ export const RenderedDocBlock = memo(DocBlock);
159
167
  */
160
168
  export function RenderedDocs({ language }: { language: EditorLanguage }) {
161
169
  const feature = language.renderedDocs;
170
+
162
171
  const getEntries = () => feature.getEntries();
172
+
163
173
  const entries = useSyncExternalStore((listener) => feature.subscribe(listener), getEntries, getEntries);
164
174
  const linkable = feature.canFollow;
175
+
165
176
  return (
166
177
  <>
167
178
  {entries.map((entry) =>
@@ -4,7 +4,7 @@ import { parseChord } from '@adecore/editor/keymap';
4
4
  import type { EditorLineHighlight } from '@adecore/editor';
5
5
  import type { DiffSplit } from '@adecore/merge';
6
6
  import type { DiffViewProps } from './DiffView.tsx';
7
- import type { DiffSourceSide } from './diff-syntax.ts';
7
+ import { repaintOnTokens, type DiffSourceSide } from './diff-syntax.ts';
8
8
  import { DiffOverlays, type MountedDiff } from './diff-overlays.tsx';
9
9
  import { wordMarks } from './diff-wordmarks.ts';
10
10
  import { RowHost, LineActionHost } from './row-host.ts';
@@ -52,19 +52,25 @@ export function SplitDiffView({
52
52
  const [failure, setFailure] = useState<{ error: unknown } | null>(null);
53
53
  const [mounted, setMounted] = useState<MountedSplit | null>(null);
54
54
  const [height, setHeight] = useState(0);
55
+
55
56
  useLayoutEffect(() => {
56
57
  current.current = { display, tokens, theme, wrap, label };
57
58
  });
59
+
58
60
  useLayoutEffect(() => {
59
61
  const oldHost = oldElement.current;
60
62
  const newHost = newElement.current;
63
+
61
64
  if (oldHost === null || newHost === null) {
62
65
  return;
63
66
  }
67
+
64
68
  const initial = current.current;
65
69
  applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
70
+
66
71
  const mount = (side: DiffSourceSide, host: HTMLElement): MountedDiff => {
67
72
  const initial = current.current;
73
+
68
74
  const editor = engine.mount(host, {
69
75
  text: initial.display[side].text,
70
76
  theme: initial.theme,
@@ -76,21 +82,29 @@ export function SplitDiffView({
76
82
  foldOutline: 'off',
77
83
  handBack: [parseChord('Mod+Enter')]
78
84
  });
85
+
79
86
  editor.setBlocks([]);
87
+
80
88
  editor.setLineTokens((line) => {
81
89
  const row = current.current.display[side].rows[line];
90
+
82
91
  return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
83
92
  });
93
+
84
94
  return { engine, editor, rows: new RowHost(editor, 'diff-alignment'), actions: new LineActionHost(editor, 'diff-status') };
85
95
  };
96
+
86
97
  const old = mount('old', oldHost);
87
98
  let fresh: MountedDiff;
99
+
88
100
  try {
89
101
  fresh = mount('new', newHost);
90
102
  } catch (error) {
91
103
  old.editor.dispose();
104
+
92
105
  throw error;
93
106
  }
107
+
94
108
  const sides = { old, new: fresh };
95
109
  let stopped = false;
96
110
  let pending = false;
@@ -98,37 +112,48 @@ export function SplitDiffView({
98
112
  const scroll = new SplitScroll({ old: old.editor, new: fresh.editor });
99
113
  let aligning = false;
100
114
  let updating = false;
115
+
101
116
  const fail = (error: unknown): void => {
102
117
  failed = true;
103
118
  setFailure({ error });
104
119
  };
120
+
105
121
  const measure = (): void => setHeight(Math.max(sides.old.editor.getContentHeight(), sides.new.editor.getContentHeight()));
122
+
106
123
  const align = (): void => {
107
124
  if (pending || stopped || failed) {
108
125
  return;
109
126
  }
127
+
110
128
  pending = true;
129
+
111
130
  // Intrinsic measurements arrive after each render; align both editors before the next paint.
112
131
  queueMicrotask(() => {
113
132
  pending = false;
133
+
114
134
  if (stopped || failed) {
115
135
  return;
116
136
  }
137
+
117
138
  aligning = true;
139
+
118
140
  try {
119
141
  const rows = splitAlignment(
120
142
  current.current.display.aligned,
121
143
  (line) => sides.old.editor.getLineHeight(line),
122
144
  (line) => sides.new.editor.getLineHeight(line)
123
145
  );
146
+
124
147
  sides.old.rows.set(rows.old);
125
148
  sides.new.rows.set(rows.new);
126
149
  } catch (error) {
127
150
  fail(error);
151
+
128
152
  return;
129
153
  } finally {
130
154
  aligning = false;
131
155
  }
156
+
132
157
  try {
133
158
  measure();
134
159
  scroll.sync();
@@ -137,21 +162,26 @@ export function SplitDiffView({
137
162
  }
138
163
  });
139
164
  };
165
+
140
166
  const update = (run: () => void): void => {
141
167
  updating = true;
168
+
142
169
  try {
143
170
  run();
144
171
  } finally {
145
172
  updating = false;
146
173
  }
174
+
147
175
  align();
148
176
  };
177
+
149
178
  const stops = SIDES.flatMap((side) => [
150
179
  sides[side].editor.onLineHeights(align),
151
180
  sides[side].editor.onViewChange(() => {
152
181
  if (stopped || failed || aligning || updating) {
153
182
  return;
154
183
  }
184
+
155
185
  try {
156
186
  measure();
157
187
  scroll.changed(side);
@@ -160,35 +190,46 @@ export function SplitDiffView({
160
190
  }
161
191
  })
162
192
  ]);
193
+
163
194
  setMounted({ old: sides.old, new: sides.new, align, update, drive: (side) => scroll.drive(side) });
164
195
  measure();
196
+
165
197
  return () => {
166
198
  stopped = true;
199
+
167
200
  for (const stop of stops) {
168
201
  stop();
169
202
  }
203
+
170
204
  for (const side of SIDES) {
171
205
  sides[side].editor.dispose();
172
206
  }
173
207
  };
174
208
  }, [engine]);
209
+
175
210
  useLayoutEffect(() => {
176
211
  if (mounted === null || mounted.old.engine !== engine) {
177
212
  return;
178
213
  }
214
+
179
215
  mounted.update(() => {
180
216
  for (const side of SIDES) {
181
217
  const { editor, actions } = mounted[side];
182
218
  editor.setText(display[side].text);
219
+
183
220
  editor.setLineNumbers((line) => {
184
221
  const row = current.current.display[side].rows[line];
222
+
185
223
  return row === undefined ? null : String(row[side]!.number);
186
224
  });
225
+
187
226
  const highlights: EditorLineHighlight[] = [];
227
+
188
228
  for (const [index, row] of display[side].rows.entries()) {
189
229
  if (row.change === 'context') {
190
230
  continue;
191
231
  }
232
+
192
233
  highlights.push({
193
234
  startLine: index + 1,
194
235
  endLine: index + 1,
@@ -197,106 +238,102 @@ export function SplitDiffView({
197
238
  emphasis: [emphasis[side].get(row[side]!.index) ?? []]
198
239
  });
199
240
  }
241
+
200
242
  editor.setLineHighlights(highlights);
243
+
201
244
  editor.setChangeMarks(
202
245
  highlights.map((highlight) => ({ startLine: highlight.startLine, endLine: highlight.endLine, kind: side === 'old' ? 'deleted' : 'added' }))
203
246
  );
247
+
204
248
  actions.set(display[side].rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
205
249
  }
206
250
  });
207
251
  }, [mounted, engine, display, emphasis]);
252
+
208
253
  useLayoutEffect(() => {
209
254
  if (mounted === null || mounted.old.engine !== engine) {
210
255
  return;
211
256
  }
257
+
212
258
  for (const side of SIDES) {
213
259
  mounted[side].editor.setLineLabels((line) => {
214
260
  const row = display[side].rows[line];
261
+
215
262
  if (row === undefined || row.change === 'context') {
216
263
  return null;
217
264
  }
265
+
218
266
  return t(side === 'old' ? 'diff.removed' : 'diff.added', { number: row[side]!.number });
219
267
  });
220
268
  }
221
269
  }, [mounted, engine, display, t]);
270
+
222
271
  useLayoutEffect(() => {
223
272
  if (mounted === null || mounted.old.engine !== engine) {
224
273
  return;
225
274
  }
275
+
226
276
  mounted.update(() => {
227
277
  for (const side of SIDES) {
228
278
  const previous = applied.current;
279
+
229
280
  if (previous.theme !== theme) {
230
281
  mounted[side].editor.setTheme(theme);
231
282
  }
283
+
232
284
  if (previous.wrap !== wrap) {
233
285
  mounted[side].editor.setWrap(wrap);
234
286
  }
287
+
235
288
  if (previous.label !== label) {
236
289
  mounted[side].editor.setLabel(label);
237
290
  }
238
291
  }
239
292
  });
293
+
240
294
  applied.current = { theme, wrap, label };
241
295
  }, [mounted, engine, theme, wrap, label]);
296
+
242
297
  useLayoutEffect(() => {
243
298
  if (mounted === null || mounted.old.engine !== engine) {
244
299
  return;
245
300
  }
301
+
246
302
  const refresh = (): void => {
247
303
  for (const side of SIDES) {
248
304
  mounted[side].editor.setLineTokens((line) => {
249
305
  const row = current.current.display[side].rows[line];
306
+
250
307
  return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
251
308
  });
252
309
  }
253
310
  };
311
+
254
312
  refresh();
255
- const ownerWindow = oldElement.current?.ownerDocument.defaultView;
256
- let frame: number | undefined;
257
- let pending = false;
258
- let stopped = false;
259
- const schedule = (): void => {
260
- if (pending) {
261
- return;
262
- }
263
- pending = true;
264
- const paint = (): void => {
265
- pending = false;
266
- if (!stopped) {
267
- refresh();
268
- }
269
- };
270
- if (ownerWindow?.requestAnimationFrame !== undefined) {
271
- frame = ownerWindow.requestAnimationFrame(paint);
272
- } else {
273
- queueMicrotask(paint);
274
- }
275
- };
276
- const stop = tokens?.subscribe(schedule);
277
- return () => {
278
- stopped = true;
279
- stop?.();
280
- if (frame !== undefined) {
281
- ownerWindow?.cancelAnimationFrame?.(frame);
282
- }
283
- };
313
+
314
+ return repaintOnTokens(oldElement.current?.ownerDocument.defaultView, tokens, refresh);
284
315
  }, [mounted, engine, display, tokens]);
316
+
285
317
  useLayoutEffect(() => {
286
318
  if (mounted === null || mounted.old.engine !== engine) {
287
319
  return;
288
320
  }
321
+
289
322
  const stops = SIDES.map((side) => onMount?.(mounted[side].editor, side));
323
+
290
324
  return () => {
291
325
  for (const stop of stops) {
292
326
  stop?.();
293
327
  }
294
328
  };
295
329
  }, [mounted, engine, onMount]);
330
+
296
331
  const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
332
+
297
333
  if (failure !== null) {
298
334
  throw failure.error;
299
335
  }
336
+
300
337
  return (
301
338
  <div
302
339
  className={`adecore-editor-diff adecore-editor-diff-split ${className ?? ''}`}
@@ -306,32 +343,30 @@ export function SplitDiffView({
306
343
  data-scroll={fill || height > limit}
307
344
  style={{ height: fill ? '100%' : Math.min(height, limit) }}
308
345
  >
309
- {SIDES.map((side) => (
310
- <div
311
- key={side}
312
- className="adecore-editor-diff-side"
313
- data-side={side}
314
- role="group"
315
- aria-label={t(`diff.${side}Side`)}
316
- onWheelCapture={() => {
317
- mounted?.drive(side);
318
- }}
319
- onFocusCapture={() => {
320
- mounted?.drive(side);
321
- }}
322
- onPointerDownCapture={() => {
323
- mounted?.drive(side);
324
- }}
325
- onKeyDownCapture={() => {
326
- mounted?.drive(side);
327
- }}
328
- >
329
- <div ref={side === 'old' ? oldElement : newElement} className="adecore-editor-diff-surface" />
330
- {mounted !== null && mounted[side].engine === engine && (
331
- <DiffOverlays mounted={mounted[side]} display={display[side]} unfold={unfold} blocked={blocked} />
332
- )}
333
- </div>
334
- ))}
346
+ {SIDES.map((side) => {
347
+ const drive = (): void => {
348
+ mounted?.drive(side);
349
+ };
350
+
351
+ return (
352
+ <div
353
+ key={side}
354
+ className="adecore-editor-diff-side"
355
+ data-side={side}
356
+ role="group"
357
+ aria-label={t(`diff.${side}Side`)}
358
+ onWheelCapture={drive}
359
+ onFocusCapture={drive}
360
+ onPointerDownCapture={drive}
361
+ onKeyDownCapture={drive}
362
+ >
363
+ <div ref={side === 'old' ? oldElement : newElement} className="adecore-editor-diff-surface" />
364
+ {mounted !== null && mounted[side].engine === engine && (
365
+ <DiffOverlays mounted={mounted[side]} display={display[side]} unfold={unfold} blocked={blocked} />
366
+ )}
367
+ </div>
368
+ );
369
+ })}
335
370
  </div>
336
371
  );
337
372
  }