@adecore/editor-react 0.22.0 → 0.23.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 (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
@@ -1,8 +1,8 @@
1
- import { useEffect, useMemo, useState } from 'react';
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Button } from '@adecore/ui';
4
4
  import { formatNumber } from '@adecore/ui/format';
5
- import type { Editor, EditorFindState } from '@adecore/editor';
5
+ import { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS, type Editor, type EditorFindState } from '@adecore/editor';
6
6
  import { compileFind, EMPTY_FIND_QUERY, type FindQuery } from './find-query.ts';
7
7
 
8
8
  export function FindReplace({ editor, className, onClose }: { editor: Editor; className?: string; onClose?(): void }) {
@@ -11,6 +11,7 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
11
11
  const [replacement, setReplacement] = useState('');
12
12
  const [preserveCase, setPreserveCase] = useState(false);
13
13
  const [state, setState] = useState<EditorFindState>({ count: 0, current: null });
14
+ const root = useRef<HTMLDivElement>(null);
14
15
  const compiled = useMemo(() => compileFind(query), [query]);
15
16
  useEffect(() => editor.onFind(setState), [editor]);
16
17
  useEffect(() => {
@@ -26,14 +27,56 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
26
27
  },
27
28
  [editor]
28
29
  );
30
+ // The host closes the widget by unmounting it, with the keyboard in it or not; only a keyboard in it is handed back.
31
+ useLayoutEffect(
32
+ () => () => {
33
+ if (root.current?.contains(root.current.ownerDocument.activeElement) === true) {
34
+ editor.focus();
35
+ }
36
+ },
37
+ [editor]
38
+ );
39
+ const failure = state.failed ?? state.captureFailed;
29
40
  const label =
30
41
  compiled.kind === 'invalid'
31
42
  ? t('find.invalid')
32
- : state.noSelection
33
- ? t('find.noSelection')
34
- : t('find.count', { total: formatNumber(state.count), current: state.current === null ? '0' : formatNumber(state.current + 1) });
43
+ : state.replaceFailed !== undefined
44
+ ? t(`find.replace.failed.${state.replaceFailed}`, {
45
+ edits: formatNumber(MAX_REPLACEMENT_PAGE),
46
+ characters: formatNumber(MAX_REPLACEMENT_CHARACTERS)
47
+ })
48
+ : failure !== undefined
49
+ ? t(`find.failed.${failure}`)
50
+ : state.searching
51
+ ? t('find.searching')
52
+ : state.large
53
+ ? t('find.large')
54
+ : state.noSelection
55
+ ? t('find.noSelection')
56
+ : state.replaced !== undefined
57
+ ? t('find.replaced', { count: formatNumber(state.replaced) })
58
+ : state.limited && state.current === null
59
+ ? t('find.countMore', { total: formatNumber(state.count) })
60
+ : t(state.limited ? 'find.countLimited' : 'find.count', {
61
+ total: formatNumber(state.count),
62
+ current: state.current === null ? '0' : formatNumber(state.current + 1)
63
+ });
35
64
  return (
36
- <div className={`adecore-editor-find ${className ?? ''}`} role="search" aria-label={t('find.label')}>
65
+ <div
66
+ ref={root}
67
+ className={`adecore-editor-find ${className ?? ''}`}
68
+ role="search"
69
+ aria-label={t('find.label')}
70
+ onKeyDown={(event) => {
71
+ if (event.key === 'Escape' && !event.nativeEvent.isComposing) {
72
+ event.preventDefault();
73
+ editor.endFind();
74
+ editor.setReplacePreview(null);
75
+ onClose?.();
76
+ editor.focus();
77
+ }
78
+ }}
79
+ >
37
80
  <div className="adecore-editor-find-row">
38
81
  <input
39
82
  type="text"
@@ -48,11 +91,6 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
48
91
  event.preventDefault();
49
92
  editor.findStep(event.shiftKey ? -1 : 1);
50
93
  }
51
- if (event.key === 'Escape') {
52
- event.preventDefault();
53
- onClose?.();
54
- editor.focus();
55
- }
56
94
  }}
57
95
  />
58
96
  <output aria-live="polite">{label}</output>
@@ -77,10 +115,10 @@ export function FindReplace({ editor, className, onClose }: { editor: Editor; cl
77
115
  <input type="checkbox" checked={preserveCase} onChange={(event) => setPreserveCase(event.target.checked)} />
78
116
  {t('find.replace.preserveCase')}
79
117
  </label>
80
- <Button size="xs" onClick={() => editor.replace(replacement, { preserveCase })}>
118
+ <Button size="xs" disabled={state.searching === true} onClick={() => editor.replace(replacement, { preserveCase })}>
81
119
  {t('find.replace.one')}
82
120
  </Button>
83
- <Button size="xs" onClick={() => editor.replaceAll(replacement, { preserveCase })}>
121
+ <Button size="xs" disabled={state.searching === true} onClick={() => void editor.replaceAllAsync(replacement, { preserveCase })}>
84
122
  {t('find.replace.allLabel')}
85
123
  </Button>
86
124
  </div>
package/src/HoverCard.tsx CHANGED
@@ -108,28 +108,30 @@ export function HoverCard({
108
108
  void language.navigation.goToName(name, anchor, name === (declared ?? info.word) ? info.definition : null);
109
109
  }}
110
110
  />
111
- <div className="flex flex-col gap-0.5 px-3 py-1.5 text-xs">
112
- <div className="flex items-center gap-3 whitespace-nowrap">
113
- {info.definition !== null && (
114
- <button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
115
- {t('language.hover.definition')}
116
- </button>
117
- )}
118
- {info.references !== null && info.references > 0 && (
119
- <button
120
- type="button"
121
- className="text-accent hover:underline"
122
- onClick={() => {
123
- language.hover.hide();
124
- void language.peek.open(position);
125
- }}
126
- >
127
- {t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
128
- </button>
129
- )}
111
+ {info.plain !== true && (
112
+ <div className="flex flex-col gap-0.5 px-3 py-1.5 text-xs">
113
+ <div className="flex items-center gap-3 whitespace-nowrap">
114
+ {info.definition !== null && (
115
+ <button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
116
+ {t('language.hover.definition')}
117
+ </button>
118
+ )}
119
+ {info.references !== null && info.references > 0 && (
120
+ <button
121
+ type="button"
122
+ className="text-accent hover:underline"
123
+ onClick={() => {
124
+ language.hover.hide();
125
+ void language.peek.open(position);
126
+ }}
127
+ >
128
+ {t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
129
+ </button>
130
+ )}
131
+ </div>
132
+ {info.definition !== null && <DefinitionPlace definition={info.definition} uri={language.uri} folder={language.project.folder} />}
130
133
  </div>
131
- {info.definition !== null && <DefinitionPlace definition={info.definition} uri={language.uri} folder={language.project.folder} />}
132
- </div>
134
+ )}
133
135
  </>
134
136
  )}
135
137
  {problems.map((problem, index) => (
@@ -2,6 +2,7 @@ import { useEffect, useReducer, type ReactNode } from 'react';
2
2
  import { useStore } from 'zustand';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { AnchoredPopup } from './AnchoredPopup.tsx';
5
+ import { CallHierarchyPanel } from './CallHierarchyPanel.tsx';
5
6
  import { CodeAuthorsCard } from './CodeAuthorsCard.tsx';
6
7
  import { CompletionPopup } from './CompletionPopup.tsx';
7
8
  import { EditorContextMenu } from './EditorContextMenu.tsx';
@@ -9,6 +10,7 @@ import { HoverCard } from './HoverCard.tsx';
9
10
  import { PeekPanel } from './PeekPanel.tsx';
10
11
  import { PickPopup } from './PickPopup.tsx';
11
12
  import { RenameCard } from './RenameCard.tsx';
13
+ import { RenderedDocs } from './RenderedDocs.tsx';
12
14
  import { SymbolPicker } from './SymbolPicker.tsx';
13
15
  import { SignatureCard } from './SignatureCard.tsx';
14
16
 
@@ -25,6 +27,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
25
27
  const pick = useStore(language.popups, (state) => state.pick);
26
28
  const rename = useStore(language.popups, (state) => state.rename);
27
29
  const peek = useStore(language.popups, (state) => state.peek);
30
+ const callHierarchy = useStore(language.popups, (state) => state.callHierarchy ?? null);
28
31
  const symbols = useStore(language.popups, (state) => state.symbols);
29
32
  const menu = useStore(language.popups, (state) => state.menu);
30
33
  const authors = useStore(language.popups, (state) => state.authors);
@@ -39,6 +42,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
39
42
  const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
40
43
  return (
41
44
  <>
45
+ <RenderedDocs language={language} />
42
46
  {hover !== null && hoverRect !== null && (
43
47
  <AnchoredPopup rect={hoverRect} onPointerEnter={() => language.hover.holdCard(true)} onPointerLeave={() => language.hover.holdCard(false)}>
44
48
  <HoverCard language={language} problems={hover.problems} info={hover.info} anchor={hover.anchor} position={hover.position} />
@@ -58,6 +62,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
58
62
  )}
59
63
  {symbols !== null && <SymbolPicker language={language} view={symbols} />}
60
64
  {peek !== null && <PeekPanel language={language} view={peek} />}
65
+ {callHierarchy !== null && <CallHierarchyPanel language={language} view={callHierarchy} />}
61
66
  {authors !== null && <CodeAuthorsCard language={language} view={authors} />}
62
67
  {rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
63
68
  </>
package/src/Markdown.tsx CHANGED
@@ -1,11 +1,18 @@
1
- import ReactMarkdown from 'react-markdown';
1
+ import ReactMarkdown, { type Components } from 'react-markdown';
2
2
  import remarkGfm from 'remark-gfm';
3
3
  import remarkBreaks from 'remark-breaks';
4
4
 
5
- export function Markdown({ text }: { text: string }) {
5
+ /* An image becomes its alt text, so a comment cannot make the editor fetch a page of its choosing. */
6
+ const WITHOUT_IMAGES: Components = {
7
+ img: ({ alt }) => <>{alt ?? ''}</>
8
+ };
9
+
10
+ export function Markdown({ text, images = true }: { text: string; images?: boolean }) {
6
11
  return (
7
12
  <div className="adecore-editor-markdown">
8
- <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>{text}</ReactMarkdown>
13
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} components={images ? undefined : WITHOUT_IMAGES}>
14
+ {text}
15
+ </ReactMarkdown>
9
16
  </div>
10
17
  );
11
18
  }
package/src/PeekPanel.tsx CHANGED
@@ -19,10 +19,9 @@ const CODE =
19
19
  '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';
20
20
 
21
21
  /* The code around the active reference, drawn like the editor's own rows: its gutter, the line the reference is on and the name marked. */
22
- function Preview({ view }: { view: PeekView }) {
22
+ export function Preview({ preview }: { preview: PeekView['preview'] }) {
23
23
  const { theme, highlight } = useEditorRendering();
24
24
  const scroller = useRef<HTMLDivElement>(null);
25
- const { preview } = view;
26
25
  const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
27
26
  const path = preview === null ? null : fileUriToPath(preview.uri);
28
27
  const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
@@ -204,7 +203,7 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
204
203
  </div>
205
204
  </div>
206
205
  <div className="flex h-[180px] shrink-0">
207
- <Preview view={view} />
206
+ <Preview preview={view.preview} />
208
207
  <Places language={language} view={view} />
209
208
  </div>
210
209
  <div className="peek-hatch h-[14px]" />
package/src/PickPopup.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useLayoutEffect, useRef } from 'react';
1
+ import { useId, useLayoutEffect, useRef } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import type { EditorRect } from '@adecore/editor';
4
4
  import type { EditorLanguage } from './editor-language.ts';
@@ -39,6 +39,7 @@ function PreviewLines({ preview }: { preview: PickPreview }) {
39
39
  * the focus from the editor, so the keys keep going to it while the list is open.
40
40
  */
41
41
  export function PickPopup({ language, view, rect }: { language: EditorLanguage; view: PickView; rect: EditorRect }) {
42
+ const listId = useId();
42
43
  const card = useRef<HTMLDivElement>(null);
43
44
  const activeRow = useRef<HTMLButtonElement>(null);
44
45
 
@@ -64,16 +65,31 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
64
65
  activeRow.current?.scrollIntoView({ block: 'nearest' });
65
66
  }, [view.active]);
66
67
 
68
+ const optionIds = new Map<string, string>();
69
+ for (const group of view.groups) {
70
+ for (const row of group.rows) {
71
+ optionIds.set(row.id, `${listId}-option-${optionIds.size}`);
72
+ }
73
+ }
74
+ const activeId = optionIds.get(view.active);
75
+
76
+ // The focus stays in the text, so the input is what names the list and its active option.
77
+ useLayoutEffect(() => {
78
+ language.editor.setInputPopup({ controls: listId, activeDescendant: activeId });
79
+ return () => language.editor.setInputPopup(null);
80
+ }, [language, listId, activeId]);
81
+
67
82
  return createPortal(
68
83
  <div ref={card} className={CARD} style={{ visibility: 'hidden' }} onPointerDown={(event) => event.preventDefault()}>
69
84
  {view.title !== null && <div className="border-b border-border px-3 py-1.5 text-xs text-text-muted">{view.title}</div>}
70
- <div role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
85
+ <div id={listId} role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
71
86
  {view.groups.map((group, groupIndex) => (
72
87
  <div key={group.title ?? groupIndex} role="group" className="flex flex-col">
73
88
  {group.title !== null && <div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint">{group.title}</div>}
74
89
  {group.rows.map((row) => (
75
90
  <button
76
91
  key={row.id}
92
+ id={optionIds.get(row.id)}
77
93
  ref={row.id === view.active ? activeRow : undefined}
78
94
  type="button"
79
95
  role="option"
@@ -86,6 +86,11 @@ function Hints({ view, active, onPick }: { view: RenameView; active: number; onP
86
86
  function Preview({ language, view }: { language: EditorLanguage; view: RenameView }) {
87
87
  const { t } = useTranslation('editor');
88
88
  const folder = language.project.folder;
89
+ const operations = view.operations ?? [];
90
+ const shownPath = (uri: string): string => {
91
+ const path = fileUriToPath(uri);
92
+ return path === null ? uri : relativeTo(path, folder);
93
+ };
89
94
  return (
90
95
  <div className="flex w-[520px] max-w-full flex-col" onPointerDown={(event) => event.preventDefault()}>
91
96
  <div className="max-h-[320px] overflow-y-auto">
@@ -115,6 +120,19 @@ function Preview({ language, view }: { language: EditorLanguage; view: RenameVie
115
120
  </div>
116
121
  );
117
122
  })}
123
+ {operations.length > 0 && (
124
+ <div className="flex flex-col gap-px border-b border-border px-3 py-1.5 text-xs last:border-b-0">
125
+ {operations.map((operation, index) => (
126
+ <div key={`${operation.kind}:${operation.uri}:${index}`} className="flex items-center gap-2">
127
+ <span className="shrink-0 font-medium">{t(`language.rename.operations.${operation.kind}`)}</span>
128
+ <span className="min-w-0 truncate text-text-faint">
129
+ {shownPath(operation.uri)}
130
+ {operation.newUri === undefined ? '' : ` \u2192 ${shownPath(operation.newUri)}`}
131
+ </span>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ )}
118
136
  </div>
119
137
  <div className="flex items-center justify-end gap-2 border-t border-border p-2">
120
138
  <Button size="xs" variant="ghost" onClick={() => language.rename.cancel()}>
@@ -0,0 +1,155 @@
1
+ import { createPortal } from 'react-dom';
2
+ import { useSyncExternalStore, type MouseEvent } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import type { EditorLanguage } from './editor-language.ts';
5
+ import { DOC_LINK_PREFIX, sectionsOf, type DocTagEntry } from './doc-comment-model.ts';
6
+ import { markdownParts } from './hover-content.ts';
7
+ import { Signature } from './HoverSections.tsx';
8
+ import { shikiLanguageOf } from './language-ids.ts';
9
+ import { Markdown } from './Markdown.tsx';
10
+ import type { RenderedDocEntry } from './rendered-docs.ts';
11
+
12
+ const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
13
+ const KNOWN = new Set([
14
+ 'deprecated',
15
+ 'template',
16
+ 'param',
17
+ 'property',
18
+ 'type',
19
+ 'var',
20
+ 'return',
21
+ 'yield',
22
+ 'throws',
23
+ 'see',
24
+ 'example',
25
+ 'since',
26
+ 'version',
27
+ 'author'
28
+ ]);
29
+
30
+ /* Code in the editor's face, with a type fainter than a name. */
31
+ function Code({ children, faint = false }: { children: string; faint?: boolean }) {
32
+ return <code className={`font-mono text-code break-words ${faint ? 'text-text-faint' : 'text-text'}`}>{children}</code>;
33
+ }
34
+
35
+ /* Markdown prose and fenced code as the hover draws them: the blocks in the code face, the rest muted. */
36
+ function Prose({ text, language, linkable, compact = false }: { text: string; language: string; linkable: boolean; compact?: boolean }) {
37
+ const plain = linkable ? text : text.replace(LINK, '$1');
38
+ return (
39
+ <>
40
+ {markdownParts(plain).map((part, index) =>
41
+ part.kind === 'code' ? (
42
+ <div key={index} className="rounded-md bg-surface-sunken px-2 py-1.5">
43
+ <Signature code={part.block.code} language={part.block.language === 'text' ? language : part.block.language} />
44
+ </div>
45
+ ) : (
46
+ <div key={index} className={`[&_.adecore-editor-markdown]:text-xs ${compact ? '[&_p]:my-0' : '[&_p]:my-1'}`}>
47
+ <Markdown text={part.text} images={false} />
48
+ </div>
49
+ )
50
+ )}
51
+ </>
52
+ );
53
+ }
54
+
55
+ /* A value of a tag, in the second column of the table; `spaced` opens a new section with more room above. */
56
+ function TagRow({ tag, language, linkable, spaced }: { tag: DocTagEntry; language: string; linkable: boolean; spaced: boolean }) {
57
+ const target = tag.name === 'see' && tag.subject !== null ? tag.subject : null;
58
+ const link =
59
+ target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
60
+ return (
61
+ <dd className={`col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`}>
62
+ {tag.name === 'example' ? (
63
+ <div className="w-full rounded-md bg-surface-sunken px-2 py-1.5">
64
+ <Signature code={tag.text} language={language} />
65
+ </div>
66
+ ) : (
67
+ <>
68
+ {tag.subject !== null && target === null && <Code>{tag.subject}</Code>}
69
+ {tag.type !== null && <Code faint>{tag.type}</Code>}
70
+ {(link !== null || tag.text !== '') && (
71
+ <div className="min-w-0 grow basis-48 text-text">
72
+ <Prose
73
+ text={[link, tag.text].filter((part) => part !== null && part !== '').join(' ')}
74
+ language={language}
75
+ linkable={linkable}
76
+ compact
77
+ />
78
+ </div>
79
+ )}
80
+ </>
81
+ )}
82
+ </dd>
83
+ );
84
+ }
85
+
86
+ export function RenderedDocBlock({ language, entry }: { language: EditorLanguage; entry: RenderedDocEntry }) {
87
+ const { t } = useTranslation('editor');
88
+ const feature = language.renderedDocs;
89
+ const { description, tags } = entry.parsed;
90
+ const highlighter = shikiLanguageOf(language.languageId);
91
+ const linkable = feature.canFollow;
92
+ const sections = sectionsOf(tags);
93
+ const deprecated = sections.find((section) => section.name === 'deprecated');
94
+ const shown = sections.filter((section) => section.name !== 'deprecated');
95
+
96
+ // One handler for every link, since the links are made by the Markdown renderer and not by us.
97
+ const follow = (event: MouseEvent<HTMLDivElement>): void => {
98
+ const anchor = (event.target as HTMLElement).closest<HTMLAnchorElement>('a');
99
+ if (anchor === null) {
100
+ return;
101
+ }
102
+ event.preventDefault();
103
+ const mod = language.apple ? event.metaKey : event.ctrlKey;
104
+ const href = anchor.getAttribute('href') ?? '';
105
+ if (!mod) {
106
+ return;
107
+ }
108
+ if (href.startsWith(DOC_LINK_PREFIX)) {
109
+ feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), entry.comment);
110
+ } else if (/^(https?:|mailto:)/i.test(href)) {
111
+ window.open(href, '_blank', 'noopener,noreferrer');
112
+ }
113
+ };
114
+
115
+ return (
116
+ <div className="adecore-rendered-doc flex max-w-[520px] flex-col gap-1.5 py-1 font-sans text-xs/[18px] text-text select-text" onClick={follow}>
117
+ {deprecated !== undefined && (
118
+ <div className="border-l-2 border-status-error pl-2 text-text-muted">
119
+ <span className="font-medium text-status-error">{t('language.docs.deprecated')}</span>
120
+ {deprecated.tags.some((tag) => tag.text !== '') && (
121
+ <Prose text={deprecated.tags.map((tag) => tag.text).join(' ')} language={highlighter} linkable={linkable} />
122
+ )}
123
+ </div>
124
+ )}
125
+ {description !== '' && (
126
+ <div className="text-text">
127
+ <Prose text={description} language={highlighter} linkable={linkable} />
128
+ </div>
129
+ )}
130
+ {shown.length > 0 && (
131
+ <dl className="m-0 grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-3 gap-y-0.5">
132
+ {shown.map((section, row) => [
133
+ <dt key={`${section.name}:label`} className={`col-start-1 font-medium text-text-muted ${row > 0 ? 'mt-1' : ''}`}>
134
+ {KNOWN.has(section.name) ? t(`language.docs.tags.${section.name}`) : `${section.name}:`}
135
+ </dt>,
136
+ ...section.tags.map((tag, index) => (
137
+ <TagRow key={`${section.name}:${index}`} tag={tag} language={highlighter} linkable={linkable} spaced={row > 0 && index === 0} />
138
+ ))
139
+ ])}
140
+ </dl>
141
+ )}
142
+ </div>
143
+ );
144
+ }
145
+
146
+ /*
147
+ * The documentation comments the editor drew as rows, filled with their formatted text. It portals into
148
+ * the element the editor made for each row, so the editor decides what is in view; a comment is parsed
149
+ * only once its row is. Links in the text are followed with Cmd (Ctrl elsewhere) held, as definitions are.
150
+ */
151
+ export function RenderedDocs({ language }: { language: EditorLanguage }) {
152
+ const feature = language.renderedDocs;
153
+ useSyncExternalStore(feature.subscribe, feature.getVersion, feature.getVersion);
154
+ return <>{feature.entries().map((entry) => createPortal(<RenderedDocBlock language={language} entry={entry} />, entry.container, entry.id))}</>;
155
+ }