@adecore/editor-react 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/DiffView.tsx CHANGED
@@ -6,7 +6,7 @@ import { diffDisplay, diffSplit, type DiffDocument, type DiffDisplay } from '@ad
6
6
  import { wordMarks } from './diff-wordmarks.ts';
7
7
  import { DiffOverlays, type MountedDiff } from './diff-overlays.tsx';
8
8
  import { SplitDiffView } from './SplitDiffView.tsx';
9
- import type { DiffSourceTokens } from './diff-syntax.ts';
9
+ import { repaintOnTokens, type DiffSourceTokens } from './diff-syntax.ts';
10
10
  import { LineActionHost, RowHost } from './row-host.ts';
11
11
  import { DEFAULT_UNFOLD_LIMITS, expandDiff, type DiffLimits } from './diff-expansion.ts';
12
12
 
@@ -60,15 +60,20 @@ function UnifiedDiffView({
60
60
  const applied = useRef({ theme, wrap, label });
61
61
  const [mounted, setMounted] = useState<MountedDiff | null>(null);
62
62
  const [height, setHeight] = useState(0);
63
+
63
64
  useLayoutEffect(() => {
64
65
  current.current = { display, tokens, theme, wrap, label };
65
66
  });
67
+
66
68
  useLayoutEffect(() => {
67
69
  const host = element.current;
70
+
68
71
  if (host === null) {
69
72
  return;
70
73
  }
74
+
71
75
  const initial = current.current;
76
+
72
77
  const editor = engine.mount(host, {
73
78
  text: initial.display.text,
74
79
  theme: initial.theme,
@@ -81,38 +86,53 @@ function UnifiedDiffView({
81
86
  // Prompt cards use this chord for their primary action; a read-only review has no split-line action.
82
87
  handBack: [parseChord('Mod+Enter')]
83
88
  });
89
+
84
90
  applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
85
91
  editor.setBlocks([]);
92
+
86
93
  editor.setLineTokens((line) => {
87
94
  const { display, tokens } = current.current;
88
95
  const row = display.rows[line];
96
+
89
97
  return row === undefined ? null : tokens?.get(row.new === undefined ? 'old' : 'new', (row.new ?? row.old)!.index);
90
98
  });
99
+
91
100
  setMounted({ engine, editor, rows: new RowHost(editor, 'diff-gaps'), actions: new LineActionHost(editor, 'diff-status') });
101
+
92
102
  const measure = (): void => setHeight(editor.getContentHeight());
103
+
93
104
  const stop = editor.onViewChange(measure);
94
105
  measure();
106
+
95
107
  return () => {
96
108
  stop();
97
109
  editor.dispose();
98
110
  };
99
111
  }, [engine]);
112
+
100
113
  useLayoutEffect(() => {
101
114
  if (mounted === null || mounted.engine !== engine) {
102
115
  return;
103
116
  }
117
+
104
118
  const { editor, rows, actions } = mounted;
105
119
  editor.setText(display.text);
120
+
106
121
  editor.setLineNumbers((line) => {
107
122
  const row = current.current.display.rows[line];
123
+
108
124
  return row === undefined ? null : String((row.new ?? row.old)!.number);
109
125
  });
126
+
110
127
  const highlights: EditorLineHighlight[] = [];
128
+
111
129
  for (const [index, row] of display.rows.entries()) {
112
130
  if (row.change !== 'delete' && row.change !== 'insert') {
113
131
  continue;
114
132
  }
133
+
115
134
  const removed = row.change === 'delete';
135
+
116
136
  highlights.push({
117
137
  startLine: index + 1,
118
138
  endLine: index + 1,
@@ -121,7 +141,9 @@ function UnifiedDiffView({
121
141
  emphasis: [(removed ? emphasis.old.get(row.old!.index) : emphasis.new.get(row.new!.index)) ?? []]
122
142
  });
123
143
  }
144
+
124
145
  editor.setLineHighlights(highlights);
146
+
125
147
  editor.setChangeMarks(
126
148
  highlights.map((highlight) => ({
127
149
  startLine: highlight.startLine,
@@ -129,6 +151,7 @@ function UnifiedDiffView({
129
151
  kind: highlight.bar === 'hatched' ? 'deleted' : 'added'
130
152
  }))
131
153
  );
154
+
132
155
  rows.set(
133
156
  display.gaps.map(({ before, row }) => ({
134
157
  id: `gap-${row.id}`,
@@ -137,87 +160,79 @@ function UnifiedDiffView({
137
160
  height: 32
138
161
  }))
139
162
  );
163
+
140
164
  actions.set(display.rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
141
165
  }, [mounted, engine, display, emphasis]);
166
+
142
167
  useLayoutEffect(() => {
143
168
  if (mounted === null || mounted.engine !== engine) {
144
169
  return;
145
170
  }
171
+
146
172
  // The color of a changed line is not the only thing that says so: the gutter carries a sentence with the numbers it reads from.
147
173
  mounted.editor.setLineLabels((line) => {
148
174
  const row = display.rows[line];
175
+
149
176
  if (row?.change === 'delete') {
150
177
  return t('diff.removed', { number: row.old!.number });
151
178
  }
179
+
152
180
  if (row?.change === 'insert') {
153
181
  return t('diff.added', { number: row.new!.number });
154
182
  }
183
+
155
184
  return row?.old !== undefined && row.new !== undefined ? t('diff.unchanged', { old: row.old.number, new: row.new.number }) : null;
156
185
  });
157
186
  }, [mounted, engine, display, t]);
187
+
158
188
  useLayoutEffect(() => {
159
189
  if (mounted === null || mounted.engine !== engine) {
160
190
  return;
161
191
  }
192
+
162
193
  const previous = applied.current;
194
+
163
195
  if (previous.theme !== theme) {
164
196
  mounted.editor.setTheme(theme);
165
197
  }
198
+
166
199
  if (previous.wrap !== wrap) {
167
200
  mounted.editor.setWrap(wrap);
168
201
  }
202
+
169
203
  if (previous.label !== label) {
170
204
  mounted.editor.setLabel(label);
171
205
  }
206
+
172
207
  applied.current = { theme, wrap, label };
173
208
  }, [mounted, engine, theme, wrap, label]);
209
+
174
210
  useLayoutEffect(() => {
175
211
  if (mounted === null || mounted.engine !== engine) {
176
212
  return;
177
213
  }
214
+
178
215
  const refresh = (): void => {
179
216
  mounted.editor.setLineTokens((line) => {
180
217
  const row = current.current.display.rows[line];
218
+
181
219
  return row === undefined ? null : current.current.tokens?.get(row.new === undefined ? 'old' : 'new', (row.new ?? row.old)!.index);
182
220
  });
183
221
  };
222
+
184
223
  refresh();
185
- const ownerWindow = element.current?.ownerDocument.defaultView;
186
- let pending = false;
187
- let stopped = false;
188
- let frame: number | undefined;
189
- const schedule = (): void => {
190
- if (pending) {
191
- return;
192
- }
193
- pending = true;
194
- const paint = (): void => {
195
- pending = false;
196
- if (!stopped) {
197
- refresh();
198
- }
199
- };
200
- if (ownerWindow?.requestAnimationFrame !== undefined) {
201
- frame = ownerWindow.requestAnimationFrame(paint);
202
- } else {
203
- queueMicrotask(paint);
204
- }
205
- };
206
- const stop = tokens?.subscribe(schedule);
207
- return () => {
208
- stopped = true;
209
- stop?.();
210
- if (frame !== undefined) {
211
- ownerWindow?.cancelAnimationFrame?.(frame);
212
- }
213
- };
224
+
225
+ return repaintOnTokens(element.current?.ownerDocument.defaultView, tokens, refresh);
214
226
  }, [mounted, engine, tokens, display]);
227
+
215
228
  useLayoutEffect(() => {
216
229
  if (mounted !== null && mounted.engine === engine) {
217
230
  return onMount?.(mounted.editor);
218
231
  }
219
232
  }, [mounted, engine, onMount]);
233
+
220
234
  const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
235
+
221
236
  return (
222
237
  <div
223
238
  className={`adecore-editor-diff ${className ?? ''}`}
@@ -233,30 +248,38 @@ function UnifiedDiffView({
233
248
 
234
249
  export function DiffView(props: DiffViewProps) {
235
250
  const { document, unfoldLimits = DEFAULT_UNFOLD_LIMITS } = props;
251
+
236
252
  const [expanded, setExpanded] = useState<{ document: DiffDocument; gaps: ReadonlyMap<number, number>; blocked: ReadonlySet<number> }>(() => ({
237
253
  document,
238
254
  gaps: new Map(),
239
255
  blocked: new Set()
240
256
  }));
257
+
241
258
  const gaps = expanded.document === document ? expanded.gaps : undefined;
242
259
  const blocked = expanded.document === document ? expanded.blocked : new Set<number>();
243
260
  const projection = props.layout === 'split' ? diffSplit(document, gaps) : diffDisplay(document, gaps);
261
+
244
262
  const unfold = (id: number): void =>
245
263
  setExpanded((previous) => {
246
264
  const gaps = previous.document === document ? previous.gaps : new Map<number, number>();
247
265
  const next = expandDiff(document, gaps, id, unfoldLimits);
266
+
248
267
  return {
249
268
  document,
250
269
  gaps: next,
251
270
  blocked: new Set([...(previous.document === document ? previous.blocked : []), ...(next === gaps ? [id] : [])])
252
271
  };
253
272
  });
273
+
254
274
  if ('aligned' in projection && projection.old.lines.length > 0 && projection.new.lines.length > 0) {
255
275
  const visibleRows = projection.aligned.reduce((count, row) => count + (row.kind === 'gap' ? 0 : row.row.change === 'pair' ? 2 : 1), 0);
256
276
  const wrap = props.wrap !== false && visibleRows <= (props.splitWrapLimit ?? 2000);
277
+
257
278
  return <SplitDiffView {...props} wrap={wrap} display={projection} unfold={unfold} blocked={blocked} />;
258
279
  }
280
+
259
281
  // An absent side has no selectable text; keep additions and deletions in the unified editor.
260
282
  const display = 'aligned' in projection ? (projection.old.lines.length > 0 ? projection.old : projection.new) : projection;
283
+
261
284
  return <UnifiedDiffView {...props} display={display} unfold={unfold} blocked={blocked} />;
262
285
  }
@@ -13,11 +13,14 @@ import type { MenuView } from './popups.ts';
13
13
  export function EditorContextMenu({ language, view, children }: { language: EditorLanguage; view: MenuView; children?: ReactNode }) {
14
14
  const { t } = useTranslation('editor');
15
15
  const trigger = useRef<HTMLDivElement>(null);
16
+
16
17
  useEffect(() => {
17
18
  trigger.current?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: view.x, clientY: view.y }));
18
19
  }, [view.x, view.y]);
20
+
19
21
  const { service } = language.project;
20
22
  const groups = menuGroupsOf((method) => service.supports(method, language.uri));
23
+
21
24
  const run: Record<MenuRowId, () => void> = {
22
25
  definition: () => void language.navigation.go('definition'),
23
26
  declaration: () => void language.navigation.go('declaration'),
@@ -28,6 +31,7 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
28
31
  renameSymbol: () => void language.rename.start(),
29
32
  codeActions: () => void language.codeActions.open()
30
33
  };
34
+
31
35
  const label = (id: MenuRowId): string => {
32
36
  switch (id) {
33
37
  case 'definition':
@@ -41,8 +45,10 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
41
45
  return t(`commands.${id}`);
42
46
  }
43
47
  };
48
+
44
49
  const item = (row: MenuRowSpec): ReactNode => {
45
50
  const shortcut = row.shortcut === null ? null : language.shortcuts[row.shortcut];
51
+
46
52
  return (
47
53
  <ContextMenu.Item key={row.id} onClick={run[row.id]}>
48
54
  {label(row.id)}
@@ -50,6 +56,7 @@ export function EditorContextMenu({ language, view, children }: { language: Edit
50
56
  </ContextMenu.Item>
51
57
  );
52
58
  };
59
+
53
60
  return (
54
61
  <ContextMenu.Root onOpenChange={(open) => !open && language.contextMenu.close()}>
55
62
  <ContextMenu.Trigger ref={trigger} className="fixed top-0 left-0 h-0 w-0" aria-hidden />
@@ -50,6 +50,7 @@ const DEFAULT_INDENTATION: EditorIndentation = { tabSize: 4, insertSpaces: true
50
50
  /* The names of the fold controls in the editor's `{start}` and `{end}` placeholders, which the editor fills in per control. */
51
51
  function foldMessages(t: TFunction<'editor'>): Pick<EditorMessages, 'fold' | 'unfold'> {
52
52
  const lines = { start: '{start}', end: '{end}' };
53
+
53
54
  return { fold: t('language.fold.fold', lines), unfold: t('language.fold.unfold', lines) };
54
55
  }
55
56
 
@@ -57,10 +58,13 @@ function sameFields<T extends object>(left: T | undefined, right: T | undefined)
57
58
  if (left === right) {
58
59
  return true;
59
60
  }
61
+
60
62
  if (left === undefined || right === undefined) {
61
63
  return false;
62
64
  }
65
+
63
66
  const keys = new Set([...Object.keys(left), ...Object.keys(right)] as (keyof T)[]);
67
+
64
68
  return [...keys].every((key) => left[key] === right[key]);
65
69
  }
66
70
 
@@ -69,34 +73,44 @@ function applyChangedOptions(editor: Editor, before: EditorOptions, after: Edito
69
73
  if (before.theme !== after.theme) {
70
74
  editor.setTheme(after.theme);
71
75
  }
76
+
72
77
  if (before.readOnly !== after.readOnly || before.readOnlyReason !== after.readOnlyReason) {
73
78
  editor.setReadOnly(after.readOnly ?? false, after.readOnlyReason);
74
79
  }
80
+
75
81
  if (before.wrap !== after.wrap) {
76
82
  editor.setWrap(after.wrap ?? false);
77
83
  }
84
+
78
85
  if (!sameFields(before.indentation, after.indentation)) {
79
86
  editor.setIndentation(after.indentation ?? DEFAULT_INDENTATION);
80
87
  }
88
+
81
89
  if (!sameFields<Partial<EditorSmartKeys>>(before.smartKeys, after.smartKeys)) {
82
90
  // The setter keeps the keys left out, where the options mean the default.
83
91
  editor.setSmartKeys({ ...DEFAULT_SMART_KEYS, ...after.smartKeys });
84
92
  }
93
+
85
94
  if (before.guides !== after.guides) {
86
95
  editor.setGuides(after.guides ?? true);
87
96
  }
97
+
88
98
  if (before.stickyScroll !== after.stickyScroll) {
89
99
  editor.setStickyScroll?.(after.stickyScroll ?? true);
90
100
  }
101
+
91
102
  if (before.whitespace !== after.whitespace) {
92
103
  editor.setWhitespace(after.whitespace ?? false);
93
104
  }
105
+
94
106
  if (before.rightMargin !== after.rightMargin) {
95
107
  editor.setRightMargin(after.rightMargin ?? null);
96
108
  }
109
+
97
110
  if (before.foldOutline !== after.foldOutline) {
98
111
  editor.setFoldOutline(after.foldOutline ?? 'hover');
99
112
  }
113
+
100
114
  if (after.label !== undefined && before.label !== after.label) {
101
115
  editor.setLabel(after.label);
102
116
  }
@@ -124,24 +138,31 @@ export function EditorView({
124
138
  const applied = useRef(options);
125
139
  const holder = useMemo(() => createHolder<EditorLanguage>(), []);
126
140
  const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
141
+
127
142
  useLayoutEffect(() => {
128
143
  latest.current = { options, timers, languageOptions, onMount };
129
144
  });
145
+
130
146
  useLayoutEffect(() => {
131
147
  const host = element.current;
148
+
132
149
  if (host === null) {
133
150
  return;
134
151
  }
152
+
135
153
  const inputs = latest.current;
136
154
  const editor = engine.mount(host, { ...inputs.options, messages: { ...foldMessages(words.current), ...inputs.options.messages } });
137
155
  mounted.current = editor;
138
156
  applied.current = inputs.options;
157
+
139
158
  const attached =
140
159
  project === undefined || uri === undefined || languageId === undefined
141
160
  ? null
142
161
  : new EditorLanguage(project, editor, uri, languageId, inputs.timers, inputs.languageOptions);
162
+
143
163
  holder.set(attached);
144
164
  const stop = inputs.onMount?.(editor, attached);
165
+
145
166
  return () => {
146
167
  mounted.current = null;
147
168
  holder.set(null);
@@ -150,24 +171,31 @@ export function EditorView({
150
171
  editor.dispose();
151
172
  };
152
173
  }, [engine, project, uri, languageId, holder]);
174
+
153
175
  useLayoutEffect(() => {
154
176
  const editor = mounted.current;
177
+
155
178
  if (editor === null || applied.current === options) {
156
179
  return;
157
180
  }
181
+
158
182
  applyChangedOptions(editor, applied.current, options);
159
183
  applied.current = options;
160
184
  });
185
+
161
186
  const { fold, unfold, noMoreOccurrences } = options.messages ?? {};
187
+
162
188
  useLayoutEffect(() => {
163
189
  words.current = t;
164
190
  const localized = foldMessages(t);
191
+
165
192
  mounted.current?.setMessages({
166
193
  fold: fold ?? localized.fold,
167
194
  unfold: unfold ?? localized.unfold,
168
195
  ...(noMoreOccurrences === undefined ? {} : { noMoreOccurrences })
169
196
  });
170
197
  }, [t, fold, unfold, noMoreOccurrences]);
198
+
171
199
  return (
172
200
  <div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
173
201
  <div ref={element} style={{ position: 'absolute', inset: 0 }} />
@@ -14,12 +14,15 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
14
14
  const root = useRef<HTMLDivElement>(null);
15
15
  const compiled = compileFind(query);
16
16
  useEffect(() => editor.onFind(setState), [editor]);
17
+
17
18
  useEffect(() => {
18
19
  editor.find(compiled.kind === 'pattern' ? query : null);
19
20
  }, [editor, query, compiled]);
21
+
20
22
  useEffect(() => {
21
23
  editor.setReplacePreview(compiled.kind === 'pattern' ? replacement : null, { preserveCase });
22
24
  }, [editor, compiled, replacement, preserveCase]);
25
+
23
26
  useEffect(
24
27
  () => () => {
25
28
  editor.endFind();
@@ -27,6 +30,7 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
27
30
  },
28
31
  [editor]
29
32
  );
33
+
30
34
  // The host closes the widget by unmounting it, with the keyboard in it or not; only a keyboard in it is handed back.
31
35
  useLayoutEffect(
32
36
  () => () => {
@@ -36,7 +40,9 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
36
40
  },
37
41
  [editor]
38
42
  );
43
+
39
44
  const failure = state.failed ?? state.captureFailed;
45
+
40
46
  const label =
41
47
  compiled.kind === 'invalid'
42
48
  ? t('find.invalid')
@@ -61,6 +67,7 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
61
67
  total: formatNumber(state.count),
62
68
  current: state.current === null ? '0' : formatNumber(state.current + 1)
63
69
  });
70
+
64
71
  return (
65
72
  <div
66
73
  ref={root}
@@ -87,6 +94,7 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
87
94
  if (event.nativeEvent.isComposing) {
88
95
  return;
89
96
  }
97
+
90
98
  if (event.key === 'Enter') {
91
99
  event.preventDefault();
92
100
  editor.findStep(event.shiftKey ? -1 : 1);
package/src/HoverCard.tsx CHANGED
@@ -22,12 +22,15 @@ export function DefinitionPlace({ definition, uri, folder }: { definition: Locat
22
22
  const { t } = useTranslation('editor');
23
23
  const path = fileUriToPath(definition.uri);
24
24
  const line = formatNumber(definition.range.start.line + 1);
25
+
25
26
  if (path === null || (folder !== '' && !path.startsWith(`${folder}/`))) {
26
27
  return null;
27
28
  }
29
+
28
30
  if (definition.uri === uri) {
29
31
  return <span className="text-text-faint">{t('language.hover.line', { line })}</span>;
30
32
  }
33
+
31
34
  return (
32
35
  <span className="flex min-w-0 font-mono text-text-faint">
33
36
  <span className="truncate">{basenameOf(path)}</span>
@@ -40,9 +43,12 @@ function ProblemSection({ problem, language }: { problem: Problem; language: Edi
40
43
  const { t } = useTranslation('editor');
41
44
  const { diagnostic } = problem;
42
45
  const severity = severityOf(diagnostic);
46
+
43
47
  const label =
44
48
  diagnostic.source === undefined ? [codeLabelOf(diagnostic), problem.server].filter((part) => part !== '').join(' · ') : codeLabelOf(diagnostic);
49
+
45
50
  const supported = language.codeActions.supported;
51
+
46
52
  return (
47
53
  <div className="flex flex-col gap-1.5 px-3 py-2.5">
48
54
  <div className="flex items-start gap-2">
@@ -52,6 +58,7 @@ function ProblemSection({ problem, language }: { problem: Problem; language: Edi
52
58
  {label !== '' && <div className="pl-[22px] text-xs text-text-muted select-text">{label}</div>}
53
59
  {diagnostic.relatedInformation?.map((related, index) => {
54
60
  const path = fileUriToPath(related.location.uri);
61
+
55
62
  return (
56
63
  <div key={index} className="pl-[22px] text-xs text-text-muted">
57
64
  {path !== null && (
@@ -101,6 +108,7 @@ export function HoverCard({
101
108
  footer?: ReactNode;
102
109
  }) {
103
110
  const { t } = useTranslation('editor');
111
+
104
112
  return (
105
113
  <div className="flex w-max min-w-[280px] max-w-[min(520px,calc(100vw-16px))] flex-col divide-y divide-border">
106
114
  {info !== null && (
@@ -18,10 +18,13 @@ export function Signature({ code, language, onName }: { code: string; language:
18
18
  if (highlight === undefined) {
19
19
  return;
20
20
  }
21
+
21
22
  let alive = true;
23
+
22
24
  highlight(code, language, theme)
23
25
  .then((result) => alive && setHtml({ key, html: linked ? linkTypeNames(result) : result }))
24
26
  .catch(() => undefined);
27
+
25
28
  return () => {
26
29
  alive = false;
27
30
  };
@@ -29,13 +32,16 @@ export function Signature({ code, language, onName }: { code: string; language:
29
32
 
30
33
  const className =
31
34
  'font-mono text-code break-words whitespace-pre-wrap [&_.line]:block [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:whitespace-pre-wrap [&_code]:font-mono';
35
+
32
36
  // One handler for every name, since the names are part of the highlighted HTML and not elements of ours.
33
37
  const follow = (event: MouseEvent<HTMLDivElement>): void => {
34
38
  const name = (event.target as HTMLElement).closest<HTMLElement>('[data-symbol]')?.dataset.symbol;
39
+
35
40
  if (name !== undefined && onName !== undefined) {
36
41
  onName(name);
37
42
  }
38
43
  };
44
+
39
45
  return html?.key === key ? (
40
46
  <div className={className} onClick={follow} dangerouslySetInnerHTML={{ __html: html.html }} />
41
47
  ) : (
@@ -63,6 +69,7 @@ export function DocTags({ tags }: { tags: readonly DocTag[] }) {
63
69
  export function SymbolSection({ section, onName }: { section: HoverSection; onName?: (name: string, declared: string | null) => void }) {
64
70
  const declared = declaredNameOf(section.signatures[0]?.code ?? '');
65
71
  const follow = onName === undefined ? undefined : (name: string): void => onName(name, declared);
72
+
66
73
  return (
67
74
  <div className="flex flex-col gap-1.5 px-3 py-2">
68
75
  {section.title !== null && <div className="truncate font-mono text-xs text-text-faint select-text">{section.title}</div>}
@@ -113,10 +120,13 @@ export function SourceLine({ text, language }: { text: string; language: string
113
120
  if (!highlighted || highlight === undefined) {
114
121
  return;
115
122
  }
123
+
116
124
  let alive = true;
125
+
117
126
  highlight(`<?php ${text}`, 'php', theme)
118
127
  .then((result) => alive && setHtml({ key, html: phpWithoutOpenTag(result) }))
119
128
  .catch(() => undefined);
129
+
120
130
  return () => {
121
131
  alive = false;
122
132
  };
@@ -124,6 +134,7 @@ export function SourceLine({ text, language }: { text: string; language: string
124
134
 
125
135
  const className =
126
136
  'block overflow-hidden text-left font-mono text-code text-ellipsis whitespace-nowrap [direction:rtl] [&_code]:font-mono [&_pre]:m-0 [&_pre]:inline [&_pre]:bg-transparent! [&_pre]:p-0 [&_code]:inline [&_.line]:inline';
137
+
127
138
  return (
128
139
  <Tooltip label={text}>
129
140
  {highlighted && html?.key === key ? (
@@ -53,6 +53,7 @@ export function LanguagePopups<Language extends EditorLanguage>({ language, cont
53
53
  const pickRect = pick === null ? null : language.editor.rectAt(pick.anchor);
54
54
  const renameRect = rename === null ? null : language.editor.rectAt(rename.range.start);
55
55
  const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
56
+
56
57
  return (
57
58
  <>
58
59
  <RenderedDocs language={language} />
@@ -20,17 +20,21 @@ export function PanelResizeHandle({ container, height, onHeight, onCommit }: Pro
20
20
  useEffect(() => {
21
21
  const viewport = container.closest<HTMLElement>('.se-viewport');
22
22
  const Observer = viewport?.ownerDocument.defaultView?.ResizeObserver;
23
+
23
24
  if (viewport === null || viewport === undefined || Observer === undefined) {
24
25
  return;
25
26
  }
27
+
26
28
  const observer = new Observer(() => setMax(panelBodyMax(availableHeightOf(container))));
27
29
  observer.observe(viewport);
30
+
28
31
  return () => observer.disconnect();
29
32
  }, [container]);
30
33
 
31
34
  function settle(value: number, from: number): void {
32
35
  const next = clampPanelBody(value, availableHeightOf(container));
33
36
  onHeight(next);
37
+
34
38
  if (next !== from) {
35
39
  onCommit(next);
36
40
  revealRow(realTimers, container);
@@ -45,6 +49,7 @@ export function PanelResizeHandle({ container, height, onHeight, onCommit }: Pro
45
49
  if (event.button !== 0) {
46
50
  return;
47
51
  }
52
+
48
53
  event.currentTarget.setPointerCapture(event.pointerId);
49
54
  drag.current = { startY: event.clientY, startHeight: height, lastY: event.clientY };
50
55
  setDragging(true);
@@ -62,6 +67,7 @@ export function PanelResizeHandle({ container, height, onHeight, onCommit }: Pro
62
67
  if (drag.current === null) {
63
68
  return;
64
69
  }
70
+
65
71
  const { startHeight } = drag.current;
66
72
  const next = heightAt(drag.current.lastY);
67
73
  drag.current = null;
@@ -74,9 +80,11 @@ export function PanelResizeHandle({ container, height, onHeight, onCommit }: Pro
74
80
  const step = event.shiftKey ? PANEL_KEY_STEP * 4 : PANEL_KEY_STEP;
75
81
  const end = panelBodyMax(availableHeightOf(container));
76
82
  const target = { ArrowUp: height - step, ArrowDown: height + step, Home: PANEL_BODY_MIN, End: end }[event.key];
83
+
77
84
  if (target === undefined) {
78
85
  return;
79
86
  }
87
+
80
88
  event.preventDefault();
81
89
  event.stopPropagation();
82
90
  settle(target, height);
package/src/PathText.tsx CHANGED
@@ -8,6 +8,7 @@ export function PathText({ path, className = '' }: { path: string; className?: s
8
8
  if (path === '') {
9
9
  return null;
10
10
  }
11
+
11
12
  return (
12
13
  <Tooltip label={path}>
13
14
  <span className={`min-w-0 overflow-hidden text-left text-ellipsis whitespace-nowrap [direction:rtl] ${className}`}>