@adecore/editor-react 0.0.1 → 0.17.0-beta.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 (264) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +55 -0
  3. package/dist/AnchoredPopup.d.ts +12 -0
  4. package/dist/AnchoredPopup.js +27 -0
  5. package/dist/AttributionCard.d.ts +14 -0
  6. package/dist/AttributionCard.js +5 -0
  7. package/dist/BaselineStatus.d.ts +4 -0
  8. package/dist/BaselineStatus.js +13 -0
  9. package/dist/ChangeReview.d.ts +12 -0
  10. package/dist/ChangeReview.js +27 -0
  11. package/dist/CodeAuthorsCard.d.ts +6 -0
  12. package/dist/CodeAuthorsCard.js +22 -0
  13. package/dist/CompletionPopup.d.ts +8 -0
  14. package/dist/CompletionPopup.js +103 -0
  15. package/dist/EditorContextMenu.d.ts +8 -0
  16. package/dist/EditorContextMenu.js +23 -0
  17. package/dist/EditorView.d.ts +15 -0
  18. package/dist/EditorView.js +27 -0
  19. package/dist/FindReplace.d.ts +6 -0
  20. package/dist/FindReplace.js +44 -0
  21. package/dist/HoverCard.d.ts +11 -0
  22. package/dist/HoverCard.js +38 -0
  23. package/dist/HoverSections.d.ts +21 -0
  24. package/dist/HoverSections.js +82 -0
  25. package/dist/LanguagePopups.d.ts +4 -0
  26. package/dist/LanguagePopups.js +38 -0
  27. package/dist/Markdown.d.ts +3 -0
  28. package/dist/Markdown.js +7 -0
  29. package/dist/PathText.d.ts +4 -0
  30. package/dist/PathText.js +12 -0
  31. package/dist/PeekPanel.d.ts +6 -0
  32. package/dist/PeekPanel.js +111 -0
  33. package/dist/PickPopup.d.ts +8 -0
  34. package/dist/PickPopup.js +33 -0
  35. package/dist/RenameCard.d.ts +9 -0
  36. package/dist/RenameCard.js +108 -0
  37. package/dist/SignatureCard.d.ts +4 -0
  38. package/dist/SignatureCard.js +8 -0
  39. package/dist/SymbolPicker.d.ts +6 -0
  40. package/dist/SymbolPicker.js +108 -0
  41. package/dist/code-actions-model.d.ts +23 -0
  42. package/dist/code-actions-model.js +114 -0
  43. package/dist/code-actions.d.ts +29 -0
  44. package/dist/code-actions.js +329 -0
  45. package/dist/code-authors.d.ts +17 -0
  46. package/dist/code-authors.js +80 -0
  47. package/dist/code-vision-model.d.ts +20 -0
  48. package/dist/code-vision-model.js +184 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +234 -0
  51. package/dist/completion-call.d.ts +24 -0
  52. package/dist/completion-call.js +110 -0
  53. package/dist/completion-model.d.ts +51 -0
  54. package/dist/completion-model.js +353 -0
  55. package/dist/completion.d.ts +43 -0
  56. package/dist/completion.js +464 -0
  57. package/dist/conflict-model.d.ts +15 -0
  58. package/dist/conflict-model.js +36 -0
  59. package/dist/context-menu.d.ts +10 -0
  60. package/dist/context-menu.js +55 -0
  61. package/dist/definition-link.d.ts +14 -0
  62. package/dist/definition-link.js +99 -0
  63. package/dist/diagnostics-model.d.ts +15 -0
  64. package/dist/diagnostics-model.js +83 -0
  65. package/dist/diagnostics.d.ts +23 -0
  66. package/dist/diagnostics.js +95 -0
  67. package/dist/editor-language.d.ts +64 -0
  68. package/dist/editor-language.js +148 -0
  69. package/dist/editor-react.css +43 -0
  70. package/dist/find-query.d.ts +26 -0
  71. package/dist/find-query.js +51 -0
  72. package/dist/folding.d.ts +16 -0
  73. package/dist/folding.js +97 -0
  74. package/dist/highlight-layers.d.ts +10 -0
  75. package/dist/highlight-layers.js +30 -0
  76. package/dist/highlights.d.ts +16 -0
  77. package/dist/highlights.js +102 -0
  78. package/dist/history.d.ts +8 -0
  79. package/dist/history.js +47 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +43 -0
  83. package/dist/host-types.js +1 -0
  84. package/dist/hover-content.d.ts +45 -0
  85. package/dist/hover-content.js +198 -0
  86. package/dist/hover.d.ts +32 -0
  87. package/dist/hover.js +261 -0
  88. package/dist/index.d.ts +44 -0
  89. package/dist/index.js +28 -0
  90. package/dist/inlay-hints.d.ts +13 -0
  91. package/dist/inlay-hints.js +73 -0
  92. package/dist/language-ids.d.ts +3 -0
  93. package/dist/language-ids.js +49 -0
  94. package/dist/line-edits.d.ts +8 -0
  95. package/dist/line-edits.js +39 -0
  96. package/dist/locales/en.json +171 -0
  97. package/dist/locales/nl.json +171 -0
  98. package/dist/models.d.ts +20 -0
  99. package/dist/models.js +20 -0
  100. package/dist/navigation-history.d.ts +16 -0
  101. package/dist/navigation-history.js +53 -0
  102. package/dist/navigation.d.ts +22 -0
  103. package/dist/navigation.js +156 -0
  104. package/dist/paths.d.ts +1 -0
  105. package/dist/paths.js +3 -0
  106. package/dist/peek-model.d.ts +28 -0
  107. package/dist/peek-model.js +104 -0
  108. package/dist/peek.d.ts +29 -0
  109. package/dist/peek.js +216 -0
  110. package/dist/pick.d.ts +34 -0
  111. package/dist/pick.js +166 -0
  112. package/dist/popup-placement.d.ts +23 -0
  113. package/dist/popup-placement.js +34 -0
  114. package/dist/popups.d.ts +133 -0
  115. package/dist/popups.js +14 -0
  116. package/dist/project-language.d.ts +34 -0
  117. package/dist/project-language.js +187 -0
  118. package/dist/project-problems.d.ts +24 -0
  119. package/dist/project-problems.js +63 -0
  120. package/dist/proposal-model.d.ts +36 -0
  121. package/dist/proposal-model.js +197 -0
  122. package/dist/recent-choices.d.ts +10 -0
  123. package/dist/recent-choices.js +39 -0
  124. package/dist/refresher.d.ts +13 -0
  125. package/dist/refresher.js +50 -0
  126. package/dist/relative-time.d.ts +1 -0
  127. package/dist/relative-time.js +9 -0
  128. package/dist/rename-model.d.ts +25 -0
  129. package/dist/rename-model.js +93 -0
  130. package/dist/rename.d.ts +20 -0
  131. package/dist/rename.js +258 -0
  132. package/dist/rendering-context.d.ts +6 -0
  133. package/dist/rendering-context.js +5 -0
  134. package/dist/rendering.d.ts +6 -0
  135. package/dist/rendering.js +5 -0
  136. package/dist/review-group.d.ts +16 -0
  137. package/dist/review-group.js +54 -0
  138. package/dist/row-host.d.ts +42 -0
  139. package/dist/row-host.js +120 -0
  140. package/dist/selection-ranges.d.ts +7 -0
  141. package/dist/selection-ranges.js +26 -0
  142. package/dist/semantic-model.d.ts +4 -0
  143. package/dist/semantic-model.js +76 -0
  144. package/dist/semantic-tokens.d.ts +6 -0
  145. package/dist/semantic-tokens.js +40 -0
  146. package/dist/shortcut-keys.d.ts +91 -0
  147. package/dist/shortcut-keys.js +12 -0
  148. package/dist/signature-model.d.ts +18 -0
  149. package/dist/signature-model.js +33 -0
  150. package/dist/signature.d.ts +22 -0
  151. package/dist/signature.js +150 -0
  152. package/dist/snippet-session.d.ts +17 -0
  153. package/dist/snippet-session.js +110 -0
  154. package/dist/snippet.d.ts +11 -0
  155. package/dist/snippet.js +134 -0
  156. package/dist/symbol-links.d.ts +6 -0
  157. package/dist/symbol-links.js +50 -0
  158. package/dist/symbol-picker-model.d.ts +44 -0
  159. package/dist/symbol-picker-model.js +146 -0
  160. package/dist/symbol-picker.d.ts +12 -0
  161. package/dist/symbol-picker.js +77 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +97 -0
  164. package/dist/testing.d.ts +57 -0
  165. package/dist/testing.js +156 -0
  166. package/dist/timers.d.ts +14 -0
  167. package/dist/timers.js +34 -0
  168. package/dist/use-problem-counts.d.ts +3 -0
  169. package/dist/use-problem-counts.js +20 -0
  170. package/dist/view-state.d.ts +21 -0
  171. package/dist/view-state.js +63 -0
  172. package/dist/word-diff.d.ts +1 -0
  173. package/dist/word-diff.js +32 -0
  174. package/dist/workspace-edit.d.ts +21 -0
  175. package/dist/workspace-edit.js +147 -0
  176. package/package.json +74 -2
  177. package/src/AnchoredPopup.tsx +51 -0
  178. package/src/AttributionCard.tsx +36 -0
  179. package/src/BaselineStatus.tsx +20 -0
  180. package/src/ChangeReview.tsx +43 -0
  181. package/src/CodeAuthorsCard.tsx +81 -0
  182. package/src/CompletionPopup.tsx +196 -0
  183. package/src/EditorContextMenu.tsx +67 -0
  184. package/src/EditorView.tsx +45 -0
  185. package/src/FindReplace.tsx +89 -0
  186. package/src/HoverCard.tsx +121 -0
  187. package/src/HoverSections.tsx +146 -0
  188. package/src/LanguagePopups.tsx +61 -0
  189. package/src/Markdown.tsx +11 -0
  190. package/src/PathText.tsx +18 -0
  191. package/src/PeekPanel.tsx +222 -0
  192. package/src/PickPopup.tsx +102 -0
  193. package/src/RenameCard.tsx +219 -0
  194. package/src/SignatureCard.tsx +42 -0
  195. package/src/SymbolPicker.tsx +219 -0
  196. package/src/code-actions-model.ts +146 -0
  197. package/src/code-actions.ts +387 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +267 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +501 -0
  204. package/src/conflict-model.ts +56 -0
  205. package/src/context-menu.ts +63 -0
  206. package/src/definition-link.ts +106 -0
  207. package/src/diagnostics-model.ts +102 -0
  208. package/src/diagnostics.ts +117 -0
  209. package/src/editor-language.ts +163 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +112 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +57 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +49 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +281 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +87 -0
  222. package/src/language-ids.ts +54 -0
  223. package/src/line-edits.ts +48 -0
  224. package/src/locales/en.json +171 -0
  225. package/src/locales/nl.json +171 -0
  226. package/src/models.ts +157 -0
  227. package/src/navigation-history.ts +69 -0
  228. package/src/navigation.ts +169 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +248 -0
  232. package/src/pick.ts +197 -0
  233. package/src/popup-placement.ts +64 -0
  234. package/src/popups.ts +204 -0
  235. package/src/project-language.ts +220 -0
  236. package/src/project-problems.ts +87 -0
  237. package/src/proposal-model.ts +259 -0
  238. package/src/recent-choices.ts +48 -0
  239. package/src/refresher.ts +56 -0
  240. package/src/relative-time.ts +10 -0
  241. package/src/rename-model.ts +130 -0
  242. package/src/rename.ts +291 -0
  243. package/src/rendering-context.ts +12 -0
  244. package/src/rendering.tsx +6 -0
  245. package/src/review-group.ts +70 -0
  246. package/src/row-host.ts +158 -0
  247. package/src/selection-ranges.ts +32 -0
  248. package/src/semantic-model.ts +80 -0
  249. package/src/semantic-tokens.ts +48 -0
  250. package/src/shortcut-keys.ts +21 -0
  251. package/src/signature-model.ts +49 -0
  252. package/src/signature.ts +164 -0
  253. package/src/snippet-session.ts +122 -0
  254. package/src/snippet.ts +147 -0
  255. package/src/symbol-links.ts +72 -0
  256. package/src/symbol-picker-model.ts +198 -0
  257. package/src/symbol-picker.ts +86 -0
  258. package/src/symbols.ts +112 -0
  259. package/src/testing.ts +240 -0
  260. package/src/timers.ts +45 -0
  261. package/src/use-problem-counts.ts +25 -0
  262. package/src/view-state.ts +88 -0
  263. package/src/word-diff.ts +35 -0
  264. package/src/workspace-edit.ts +194 -0
@@ -0,0 +1,67 @@
1
+ import { Fragment, useEffect, useRef, type ReactNode } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ContextMenu, Kbd } from '@adecore/ui';
4
+ import type { KeymapId } from '@adecore/editor/keymap';
5
+ import type { EditorLanguage } from './editor-language.ts';
6
+ import type { MenuView } from './popups.ts';
7
+
8
+ export function EditorContextMenu({ language, view, children }: { language: EditorLanguage; view: MenuView; children?: ReactNode }) {
9
+ const { t } = useTranslation('editor');
10
+ const trigger = useRef<HTMLDivElement>(null);
11
+ useEffect(() => {
12
+ trigger.current?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: view.x, clientY: view.y }));
13
+ }, [view.x, view.y]);
14
+ const { service } = language.project;
15
+ const rows: { id: KeymapId; method: string; run(): void }[] = [
16
+ { id: 'goToDefinition', method: 'textDocument/definition', run: () => void language.navigation.go('definition') },
17
+ { id: 'goToTypeDefinition', method: 'textDocument/typeDefinition', run: () => void language.navigation.go('typeDefinition') },
18
+ { id: 'goToImplementation', method: 'textDocument/implementation', run: () => void language.navigation.go('implementation') },
19
+ { id: 'peekReferences', method: 'textDocument/references', run: () => void language.peek.open() },
20
+ { id: 'renameSymbol', method: 'textDocument/rename', run: () => void language.rename.start() },
21
+ { id: 'codeActions', method: 'textDocument/codeAction', run: () => void language.codeActions.open() }
22
+ ];
23
+ return (
24
+ <ContextMenu.Root onOpenChange={(open) => !open && language.contextMenu.close()}>
25
+ <ContextMenu.Trigger ref={trigger} className="fixed top-0 left-0 h-0 w-0" aria-hidden />
26
+ <ContextMenu.Popup>
27
+ {rows
28
+ .filter((row) => service.supports(row.method, language.uri))
29
+ .map((row) => (
30
+ <ContextMenu.Item key={row.id} onClick={row.run}>
31
+ {t(`commands.${row.id}`)}
32
+ {language.shortcuts[row.id] !== null && <Kbd shortcut={language.shortcuts[row.id]!} />}
33
+ </ContextMenu.Item>
34
+ ))}
35
+ {view.refactors.length > 0 && (
36
+ <ContextMenu.SubmenuRoot>
37
+ <ContextMenu.SubmenuTrigger>{t('language.context.refactor')}</ContextMenu.SubmenuTrigger>
38
+ <ContextMenu.Popup>
39
+ {view.refactors.map((entry, index) => (
40
+ <Fragment key={entry.id}>
41
+ {index > 0 && view.refactors[index - 1]!.group !== entry.group && <ContextMenu.Separator />}
42
+ <ContextMenu.Item onClick={() => void language.codeActions.apply(entry)}>{entry.action.title}</ContextMenu.Item>
43
+ </Fragment>
44
+ ))}
45
+ </ContextMenu.Popup>
46
+ </ContextMenu.SubmenuRoot>
47
+ )}
48
+ {children}
49
+ <ContextMenu.Separator />
50
+ {(['cut', 'copy', 'paste'] as const).map((action) => (
51
+ <ContextMenu.Item key={action} onClick={() => language.contextMenu.clipboard(action)}>
52
+ {t(`language.context.${action}`)}
53
+ </ContextMenu.Item>
54
+ ))}
55
+ {service.supports('textDocument/formatting', language.uri) && (
56
+ <>
57
+ <ContextMenu.Separator />
58
+ <ContextMenu.Item onClick={() => void language.codeActions.formatDocument()}>
59
+ {t('commands.formatDocument')}
60
+ {language.shortcuts.formatDocument !== null && <Kbd shortcut={language.shortcuts.formatDocument} />}
61
+ </ContextMenu.Item>
62
+ </>
63
+ )}
64
+ </ContextMenu.Popup>
65
+ </ContextMenu.Root>
66
+ );
67
+ }
@@ -0,0 +1,45 @@
1
+ import { createHolder } from './holder.ts';
2
+ import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type Ref } from 'react';
3
+ import type { Editor, EditorEngine, EditorOptions } from '@adecore/editor';
4
+ import { EditorLanguage } from './editor-language.ts';
5
+ import { LanguagePopups } from './LanguagePopups.tsx';
6
+ import type { ProjectLanguage } from './project-language.ts';
7
+
8
+ export interface EditorViewProps {
9
+ engine: EditorEngine;
10
+ options: EditorOptions;
11
+ project?: ProjectLanguage;
12
+ uri?: string;
13
+ languageId?: string;
14
+ className?: string;
15
+ ref?: Ref<HTMLDivElement>;
16
+ onMount?(editor: Editor, language: EditorLanguage | null): void | (() => void);
17
+ }
18
+
19
+ export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount }: EditorViewProps) {
20
+ const element = useRef<HTMLDivElement>(null);
21
+ const holder = useMemo(() => createHolder<EditorLanguage>(), []);
22
+ const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
23
+ useLayoutEffect(() => {
24
+ const host = element.current;
25
+ if (host === null) {
26
+ return;
27
+ }
28
+ const editor = engine.mount(host, options);
29
+ const attached = project === undefined || uri === undefined || languageId === undefined ? null : new EditorLanguage(project, editor, uri, languageId);
30
+ holder.set(attached);
31
+ const stop = onMount?.(editor, attached);
32
+ return () => {
33
+ holder.set(null);
34
+ stop?.();
35
+ attached?.dispose();
36
+ editor.dispose();
37
+ };
38
+ }, [engine, options, project, uri, languageId, holder, onMount]);
39
+ return (
40
+ <div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
41
+ <div ref={element} style={{ position: 'absolute', inset: 0 }} />
42
+ {language !== null && <LanguagePopups language={language} />}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,89 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@adecore/ui';
4
+ import { formatNumber } from '@adecore/ui/format';
5
+ import type { Editor, EditorFindState } from '@adecore/editor';
6
+ import { compileFind, EMPTY_FIND_QUERY, type FindQuery } from './find-query.ts';
7
+
8
+ export function FindReplace({ editor, className, onClose }: { editor: Editor; className?: string; onClose?(): void }) {
9
+ const { t } = useTranslation('editor');
10
+ const [query, setQuery] = useState<FindQuery>(EMPTY_FIND_QUERY);
11
+ const [replacement, setReplacement] = useState('');
12
+ const [preserveCase, setPreserveCase] = useState(false);
13
+ const [state, setState] = useState<EditorFindState>({ count: 0, current: null });
14
+ const compiled = useMemo(() => compileFind(query), [query]);
15
+ useEffect(() => editor.onFind(setState), [editor]);
16
+ useEffect(() => {
17
+ editor.find(compiled.kind === 'pattern' ? query : null);
18
+ }, [editor, query, compiled]);
19
+ useEffect(() => {
20
+ editor.setReplacePreview(compiled.kind === 'pattern' ? replacement : null, { preserveCase });
21
+ }, [editor, compiled, replacement, preserveCase]);
22
+ useEffect(
23
+ () => () => {
24
+ editor.endFind();
25
+ editor.setReplacePreview(null);
26
+ },
27
+ [editor]
28
+ );
29
+ const label =
30
+ compiled.kind === 'invalid'
31
+ ? 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) });
35
+ return (
36
+ <div className={`adecore-editor-find ${className ?? ''}`} role="search" aria-label={t('find.label')}>
37
+ <div className="adecore-editor-find-row">
38
+ <input
39
+ type="text"
40
+ aria-label={t('find.label')}
41
+ value={query.text}
42
+ onChange={(event) => setQuery({ ...query, text: event.target.value })}
43
+ onKeyDown={(event) => {
44
+ if (event.nativeEvent.isComposing) {
45
+ return;
46
+ }
47
+ if (event.key === 'Enter') {
48
+ event.preventDefault();
49
+ editor.findStep(event.shiftKey ? -1 : 1);
50
+ }
51
+ if (event.key === 'Escape') {
52
+ event.preventDefault();
53
+ onClose?.();
54
+ editor.focus();
55
+ }
56
+ }}
57
+ />
58
+ <output aria-live="polite">{label}</output>
59
+ <Button size="xs" onClick={() => editor.findStep(-1)}>
60
+ {t('find.previous')}
61
+ </Button>
62
+ <Button size="xs" onClick={() => editor.findStep(1)}>
63
+ {t('find.next')}
64
+ </Button>
65
+ </div>
66
+ <div className="adecore-editor-find-row">
67
+ {(['caseSensitive', 'wholeWord', 'regex', 'inSelection'] as const).map((option) => (
68
+ <label key={option}>
69
+ <input type="checkbox" checked={query[option] === true} onChange={(event) => setQuery({ ...query, [option]: event.target.checked })} />
70
+ {t(`find.${option}`)}
71
+ </label>
72
+ ))}
73
+ </div>
74
+ <div className="adecore-editor-find-row">
75
+ <input type="text" aria-label={t('find.replace.label')} value={replacement} onChange={(event) => setReplacement(event.target.value)} />
76
+ <label>
77
+ <input type="checkbox" checked={preserveCase} onChange={(event) => setPreserveCase(event.target.checked)} />
78
+ {t('find.replace.preserveCase')}
79
+ </label>
80
+ <Button size="xs" onClick={() => editor.replace(replacement, { preserveCase })}>
81
+ {t('find.replace.one')}
82
+ </Button>
83
+ <Button size="xs" onClick={() => editor.replaceAll(replacement, { preserveCase })}>
84
+ {t('find.replace.allLabel')}
85
+ </Button>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,121 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { CircleX, Info, TriangleAlert } from 'lucide-react';
3
+ import { fileUriToPath } from '@adecore/lsp';
4
+ import { Button, Icon, Tooltip } from '@adecore/ui';
5
+ import { formatNumber } from '@adecore/ui/format';
6
+ import type { EditorPosition } from '@adecore/editor';
7
+ import { basenameOf } from './paths.ts';
8
+ import type { EditorLanguage } from './editor-language.ts';
9
+ import { codeLabelOf, severityOf, type Problem } from './diagnostics-model.ts';
10
+ import type { HoverInfo } from './popups.ts';
11
+ import { SymbolSections } from './HoverSections.tsx';
12
+
13
+ const SEVERITY_ICONS = { error: CircleX, warning: TriangleAlert, info: Info, hint: Info } as const;
14
+ const SEVERITY_COLORS = { error: 'text-status-error', warning: 'text-status-needs-you', info: 'text-status-running', hint: 'text-text-muted' } as const;
15
+
16
+ function ProblemSection({ problem, language }: { problem: Problem; language: EditorLanguage }) {
17
+ const { t } = useTranslation('editor');
18
+ const { diagnostic } = problem;
19
+ const severity = severityOf(diagnostic);
20
+ const label =
21
+ diagnostic.source === undefined ? [codeLabelOf(diagnostic), problem.server].filter((part) => part !== '').join(' · ') : codeLabelOf(diagnostic);
22
+ const supported = language.codeActions.supported;
23
+ return (
24
+ <div className="flex flex-col gap-1.5 px-3 py-2.5">
25
+ <div className="flex items-start gap-2">
26
+ <Icon icon={SEVERITY_ICONS[severity]} size={14} className={`mt-px shrink-0 ${SEVERITY_COLORS[severity]}`} />
27
+ <div className="min-w-0 text-xs/[18px] break-words whitespace-pre-wrap select-text">{diagnostic.message}</div>
28
+ </div>
29
+ {label !== '' && <div className="pl-[22px] text-xs text-text-muted select-text">{label}</div>}
30
+ {diagnostic.relatedInformation?.map((related, index) => {
31
+ const path = fileUriToPath(related.location.uri);
32
+ return (
33
+ <div key={index} className="pl-[22px] text-xs text-text-muted">
34
+ {path !== null && (
35
+ <button type="button" className="font-mono text-accent hover:underline" onClick={() => language.goTo(related.location)}>
36
+ {basenameOf(path)}:{formatNumber(related.location.range.start.line + 1)}
37
+ </button>
38
+ )}{' '}
39
+ {related.message}
40
+ </div>
41
+ );
42
+ })}
43
+ <div className="pt-1 pl-[22px]">
44
+ <Tooltip label={t(supported ? 'language.hover.quickFix' : 'language.hover.quickFixNone')}>
45
+ <Button
46
+ size="xs"
47
+ variant="secondary"
48
+ aria-disabled={!supported}
49
+ onClick={() => {
50
+ if (supported) {
51
+ language.hover.hide();
52
+ void language.codeActions.quickFixFor(problem);
53
+ }
54
+ }}
55
+ >
56
+ {t('language.hover.quickFix')}
57
+ </Button>
58
+ </Tooltip>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export function HoverCard({
65
+ language,
66
+ problems,
67
+ info,
68
+ anchor,
69
+ position
70
+ }: {
71
+ language: EditorLanguage;
72
+ problems: readonly Problem[];
73
+ info: HoverInfo | null;
74
+ anchor: EditorPosition;
75
+ position: EditorPosition;
76
+ }) {
77
+ const { t } = useTranslation('editor');
78
+ const place = info?.definition === null || info === null ? null : fileUriToPath(info.definition.uri);
79
+ return (
80
+ <div className="flex w-max min-w-[280px] max-w-[min(520px,calc(100vw-16px))] flex-col divide-y divide-border">
81
+ {info !== null && (
82
+ <>
83
+ <SymbolSections
84
+ text={info.text}
85
+ onName={(name, declared) => {
86
+ language.hover.hide();
87
+ void language.navigation.goToName(name, anchor, name === (declared ?? info.word) ? info.definition : null);
88
+ }}
89
+ />
90
+ <div className="flex items-center gap-3 px-3 py-1.5 text-xs">
91
+ {info.definition !== null && (
92
+ <button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
93
+ {t('language.hover.definition')}
94
+ </button>
95
+ )}
96
+ {info.references !== null && info.references > 0 && (
97
+ <button
98
+ type="button"
99
+ className="text-accent hover:underline"
100
+ onClick={() => {
101
+ language.hover.hide();
102
+ void language.peek.open(position);
103
+ }}
104
+ >
105
+ {t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
106
+ </button>
107
+ )}
108
+ {place !== null && info.definition !== null && (
109
+ <span className="ml-auto font-mono text-text-faint">
110
+ {basenameOf(place)}:{formatNumber(info.definition.range.start.line + 1)}
111
+ </span>
112
+ )}
113
+ </div>
114
+ </>
115
+ )}
116
+ {problems.map((problem, index) => (
117
+ <ProblemSection key={index} problem={problem} language={language} />
118
+ ))}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,146 @@
1
+ import { useEditorRendering } from './rendering-context.ts';
2
+ import { useEffect, useState, type MouseEvent } from 'react';
3
+ import { Markdown } from './Markdown.tsx';
4
+ import { Tooltip } from '@adecore/ui';
5
+ import { hoverSectionsOf, markdownParts, type DocTag, type HoverSection, type HoverText } from './hover-content.ts';
6
+ import { BaselineStatus } from './BaselineStatus.tsx';
7
+ import { declaredNameOf, linkTypeNames } from './symbol-links.ts';
8
+
9
+ /* Code as the viewer colors it, plain until the grammar is in so the card never changes size under the pointer by much. */
10
+ export function Signature({ code, language, onName }: { code: string; language: string; onName?: (name: string) => void }) {
11
+ const { theme, highlight } = useEditorRendering();
12
+ const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
13
+ const key = `${language}\0${theme}\0${code}\0${onName !== undefined}`;
14
+ // Only whether there are links goes into the highlighting; the handler is read when a name is pressed.
15
+ const linked = onName !== undefined;
16
+
17
+ useEffect(() => {
18
+ if (highlight === undefined) {
19
+ return;
20
+ }
21
+ let alive = true;
22
+ highlight(code, language, theme)
23
+ .then((result) => alive && setHtml({ key, html: linked ? linkTypeNames(result) : result }))
24
+ .catch(() => undefined);
25
+ return () => {
26
+ alive = false;
27
+ };
28
+ }, [code, language, theme, key, linked, highlight]);
29
+
30
+ const className =
31
+ 'font-mono text-code break-words whitespace-pre-wrap [&_.line]:block [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:whitespace-pre-wrap [&_code]:font-mono';
32
+ // One handler for every name, since the names are part of the highlighted HTML and not elements of ours.
33
+ const follow = (event: MouseEvent<HTMLDivElement>): void => {
34
+ const name = (event.target as HTMLElement).closest<HTMLElement>('[data-symbol]')?.dataset.symbol;
35
+ if (name !== undefined && onName !== undefined) {
36
+ onName(name);
37
+ }
38
+ };
39
+ return html?.key === key ? (
40
+ <div className={className} onClick={follow} dangerouslySetInnerHTML={{ __html: html.html }} />
41
+ ) : (
42
+ <div className={className}>{code}</div>
43
+ );
44
+ }
45
+
46
+ /* A docblock's tags as rows: the tag once beside each run of the same tag, so a list of parameters reads as one. */
47
+ export function DocTags({ tags }: { tags: readonly DocTag[] }) {
48
+ return (
49
+ <div className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-0.5 text-xs/[18px] select-text">
50
+ {tags.map((tag, index) => (
51
+ <div key={index} className="contents">
52
+ <span className="font-mono text-text-faint">{tags[index - 1]?.name === tag.name ? '' : `@${tag.name}`}</span>
53
+ <div className="min-w-0 break-words text-text-muted [&_.adecore-editor-markdown]:text-xs [&_code]:bg-transparent! [&_code]:p-0! [&_p]:m-0">
54
+ <Markdown text={tag.markdown} />
55
+ </div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ /* One symbol: its qualified name muted, its signature, the prose and examples as compact blocks, and its tags as rows. Names in the signature are links only where `onName` says what a press does. */
63
+ export function SymbolSection({ section, onName }: { section: HoverSection; onName?: (name: string, declared: string | null) => void }) {
64
+ const declared = declaredNameOf(section.signatures[0]?.code ?? '');
65
+ const follow = onName === undefined ? undefined : (name: string): void => onName(name, declared);
66
+ return (
67
+ <div className="flex flex-col gap-1.5 px-3 py-2">
68
+ {section.title !== null && <div className="truncate font-mono text-xs text-text-faint select-text">{section.title}</div>}
69
+ {section.signatures.map((block, index) => (
70
+ <Signature key={index} code={block.code} language={block.language} onName={follow} />
71
+ ))}
72
+ {section.baseline !== null && <BaselineStatus baseline={section.baseline} />}
73
+ {markdownParts(section.markdown).map((part, index) =>
74
+ part.kind === 'code' ? (
75
+ <div key={index} className="rounded-md bg-surface-sunken px-2 py-1.5">
76
+ <Signature code={part.block.code} language={part.block.language} onName={follow} />
77
+ </div>
78
+ ) : (
79
+ <div key={index} className="text-text-muted select-text [&_.adecore-editor-markdown]:text-xs [&_p]:my-1">
80
+ <Markdown text={part.text} />
81
+ </div>
82
+ )
83
+ )}
84
+ {section.tags.length > 0 && <DocTags tags={section.tags} />}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ /* What a server says about a symbol as the sections of a hover card, shared by the hover and the documentation beside the suggestions. */
90
+ export function SymbolSections({ text, onName }: { text: HoverText; onName?: (name: string, declared: string | null) => void }) {
91
+ return (
92
+ <>
93
+ {hoverSectionsOf(text).map((section, index) => (
94
+ <SymbolSection key={index} section={section} onName={onName} />
95
+ ))}
96
+ </>
97
+ );
98
+ }
99
+
100
+ /* PHP highlights only after its opening tag, which is put in front for the grammar and taken off again. */
101
+ function phpWithoutOpenTag(html: string): string {
102
+ return html.replace(/<span[^>]*>&#x3C;\?<\/span><span[^>]*>php<\/span>(<span[^>]*>) /, '$1');
103
+ }
104
+
105
+ /*
106
+ * Where a suggestion comes from, such as `use Raxos\Database\Orm\Attribute\PrimaryKey`, on one line that gives
107
+ * way from its start, so the end of the name stays readable and a word is never cut in two. The full text is in the tooltip.
108
+ */
109
+ export function SourceLine({ text, language }: { text: string; language: string }) {
110
+ const { theme, highlight } = useEditorRendering();
111
+ const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
112
+ const highlighted = language === 'php' && text.startsWith('use ');
113
+ const key = `${theme}\0${text}`;
114
+
115
+ useEffect(() => {
116
+ if (!highlighted) {
117
+ return;
118
+ }
119
+ if (highlight === undefined) {
120
+ return;
121
+ }
122
+ let alive = true;
123
+ highlight(`<?php ${text}`, 'php', theme)
124
+ .then((result) => alive && setHtml({ key, html: phpWithoutOpenTag(result) }))
125
+ .catch(() => undefined);
126
+ return () => {
127
+ alive = false;
128
+ };
129
+ }, [highlighted, text, theme, key, highlight]);
130
+
131
+ const className =
132
+ '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';
133
+ return (
134
+ <Tooltip label={text}>
135
+ {highlighted && html?.key === key ? (
136
+ <div className={className}>
137
+ <bdi dangerouslySetInnerHTML={{ __html: html.html }} />
138
+ </div>
139
+ ) : (
140
+ <div className={`${className} text-text-muted`}>
141
+ <bdi>{text}</bdi>
142
+ </div>
143
+ )}
144
+ </Tooltip>
145
+ );
146
+ }
@@ -0,0 +1,61 @@
1
+ import { useEffect, useReducer } from 'react';
2
+ import { useStore } from 'zustand';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { AnchoredPopup } from './AnchoredPopup.tsx';
5
+ import { CodeAuthorsCard } from './CodeAuthorsCard.tsx';
6
+ import { CompletionPopup } from './CompletionPopup.tsx';
7
+ import { EditorContextMenu } from './EditorContextMenu.tsx';
8
+ import { HoverCard } from './HoverCard.tsx';
9
+ import { PeekPanel } from './PeekPanel.tsx';
10
+ import { PickPopup } from './PickPopup.tsx';
11
+ import { RenameCard } from './RenameCard.tsx';
12
+ import { SymbolPicker } from './SymbolPicker.tsx';
13
+ import { SignatureCard } from './SignatureCard.tsx';
14
+
15
+ /*
16
+ * Every card a language feature opens over the editor. Each feature keeps its own state and these
17
+ * only draw it, placed by the screen position of the character it belongs to, which is read again
18
+ * whenever the editor scrolls or resizes.
19
+ */
20
+ export function LanguagePopups({ language }: { language: EditorLanguage }) {
21
+ const [, redraw] = useReducer((count: number) => count + 1, 0);
22
+ const hover = useStore(language.popups, (state) => state.hover);
23
+ const completion = useStore(language.popups, (state) => state.completion);
24
+ const signature = useStore(language.popups, (state) => state.signature);
25
+ const pick = useStore(language.popups, (state) => state.pick);
26
+ const rename = useStore(language.popups, (state) => state.rename);
27
+ const peek = useStore(language.popups, (state) => state.peek);
28
+ const symbols = useStore(language.popups, (state) => state.symbols);
29
+ const menu = useStore(language.popups, (state) => state.menu);
30
+ const authors = useStore(language.popups, (state) => state.authors);
31
+
32
+ useEffect(() => language.editor.onViewChange(redraw), [language]);
33
+
34
+ const hoverRect = hover === null ? null : language.editor.rectAt(hover.anchor);
35
+ const completionRect = completion === null ? null : language.editor.rectAt(completion.anchor);
36
+ const signatureRect = signature === null ? null : language.editor.rectAt(signature.anchor);
37
+ const pickRect = pick === null ? null : language.editor.rectAt(pick.anchor);
38
+ const renameRect = rename === null ? null : language.editor.rectAt(rename.range.start);
39
+ const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
40
+ return (
41
+ <>
42
+ {hover !== null && hoverRect !== null && (
43
+ <AnchoredPopup rect={hoverRect} onPointerEnter={() => language.hover.holdCard(true)} onPointerLeave={() => language.hover.holdCard(false)}>
44
+ <HoverCard language={language} problems={hover.problems} info={hover.info} anchor={hover.anchor} position={hover.position} />
45
+ </AnchoredPopup>
46
+ )}
47
+ {signature !== null && signatureRect !== null && (
48
+ <AnchoredPopup rect={signatureRect} placement={{ prefer: 'above' }}>
49
+ <SignatureCard model={signature.model} />
50
+ </AnchoredPopup>
51
+ )}
52
+ {completion !== null && completionRect !== null && <CompletionPopup language={language} view={completion} rect={completionRect} />}
53
+ {pick !== null && pickRect !== null && <PickPopup language={language} view={pick} rect={pickRect} />}
54
+ {menu !== null && <EditorContextMenu language={language} view={menu} />}
55
+ {symbols !== null && <SymbolPicker language={language} view={symbols} />}
56
+ {peek !== null && <PeekPanel language={language} view={peek} />}
57
+ {authors !== null && <CodeAuthorsCard language={language} view={authors} />}
58
+ {rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
59
+ </>
60
+ );
61
+ }
@@ -0,0 +1,11 @@
1
+ import ReactMarkdown from 'react-markdown';
2
+ import remarkGfm from 'remark-gfm';
3
+ import remarkBreaks from 'remark-breaks';
4
+
5
+ export function Markdown({ text }: { text: string }) {
6
+ return (
7
+ <div className="adecore-editor-markdown">
8
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>{text}</ReactMarkdown>
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,18 @@
1
+ import { Tooltip } from '@adecore/ui';
2
+
3
+ /*
4
+ * A path that gives way from its start, so the end of it, which names the place, stays readable. The
5
+ * full path is in the tooltip. The `bdi` keeps the slashes in reading order inside the right-to-left box.
6
+ */
7
+ export function PathText({ path, className = '' }: { path: string; className?: string }) {
8
+ if (path === '') {
9
+ return null;
10
+ }
11
+ return (
12
+ <Tooltip label={path}>
13
+ <span className={`min-w-0 overflow-hidden text-left text-ellipsis whitespace-nowrap [direction:rtl] ${className}`}>
14
+ <bdi>{path}</bdi>
15
+ </span>
16
+ </Tooltip>
17
+ );
18
+ }