@adecore/editor-react 0.0.1 → 0.17.0-beta.2

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 +60 -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 +16 -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 +6 -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 +30 -0
  44. package/dist/code-actions.js +326 -0
  45. package/dist/code-authors.d.ts +18 -0
  46. package/dist/code-authors.js +79 -0
  47. package/dist/code-vision-model.d.ts +21 -0
  48. package/dist/code-vision-model.js +183 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +236 -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 +463 -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 +65 -0
  68. package/dist/editor-language.js +170 -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 +95 -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 +46 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +45 -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 +260 -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 +71 -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 +23 -0
  103. package/dist/navigation.js +155 -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 +30 -0
  109. package/dist/peek.js +215 -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 +36 -0
  117. package/dist/project-language.js +190 -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 +21 -0
  131. package/dist/rename.js +257 -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 +38 -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 +76 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +95 -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 +47 -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 +65 -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 +384 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +269 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +500 -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 +189 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +110 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +56 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +52 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +280 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +85 -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 +168 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +247 -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 +223 -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 +290 -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 +46 -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 +85 -0
  258. package/src/symbols.ts +110 -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,65 @@
1
+ import { useEffect, useReducer, type ReactNode } 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. `contextMenuItems` adds rows of the app to the context menu.
19
+ */
20
+ export function LanguagePopups({ language, contextMenuItems }: { language: EditorLanguage; contextMenuItems?: (language: EditorLanguage) => ReactNode }) {
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 && (
55
+ <EditorContextMenu language={language} view={menu}>
56
+ {contextMenuItems?.(language)}
57
+ </EditorContextMenu>
58
+ )}
59
+ {symbols !== null && <SymbolPicker language={language} view={symbols} />}
60
+ {peek !== null && <PeekPanel language={language} view={peek} />}
61
+ {authors !== null && <CodeAuthorsCard language={language} view={authors} />}
62
+ {rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
63
+ </>
64
+ );
65
+ }
@@ -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
+ }
@@ -0,0 +1,222 @@
1
+ import { useEditorRendering } from './rendering-context.ts';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { ChevronDown, ChevronRight, X } from 'lucide-react';
6
+ import { fileUriToPath } from '@adecore/lsp';
7
+ import { FileIcon, IconButton, Tooltip } from '@adecore/ui';
8
+ import { formatNumber } from '@adecore/ui/format';
9
+ import { basenameOf } from './paths.ts';
10
+ import type { EditorLanguage } from './editor-language.ts';
11
+ import { shikiLanguageOfPath } from './language-ids.ts';
12
+ import { PathText } from './PathText.tsx';
13
+ import { PEEK_HEIGHT } from './peek.ts';
14
+ import { distinguishingFolders, visualColumnOf, type NameRange, type PeekPlace } from './peek-model.ts';
15
+ import type { PeekView } from './popups.ts';
16
+
17
+ const TAB_SIZE = 4;
18
+ const CODE =
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
+
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 }) {
23
+ const { theme, highlight } = useEditorRendering();
24
+ const scroller = useRef<HTMLDivElement>(null);
25
+ const { preview } = view;
26
+ const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
27
+ const path = preview === null ? null : fileUriToPath(preview.uri);
28
+ const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
29
+
30
+ useEffect(() => {
31
+ if (preview === null) {
32
+ return;
33
+ }
34
+ if (highlight === undefined) {
35
+ return;
36
+ }
37
+ let alive = true;
38
+ highlight(preview.text, shikiLanguageOfPath(path ?? preview.uri), theme)
39
+ .then((result) => alive && setHtml({ key, html: result }))
40
+ .catch(() => undefined);
41
+ return () => {
42
+ alive = false;
43
+ };
44
+ }, [preview, path, theme, key, highlight]);
45
+
46
+ useEffect(() => {
47
+ scroller.current?.scrollTo(0, 0);
48
+ }, [preview]);
49
+
50
+ if (preview === null) {
51
+ return <div className="min-w-0 flex-1" />;
52
+ }
53
+ const lines = preview.text.split('\n');
54
+ const name = preview.name === null ? null : columnsOf(lines[preview.active] ?? '', preview.name);
55
+ return (
56
+ <div ref={scroller} className="relative min-w-0 flex-1 overflow-auto pt-1">
57
+ <div className="relative flex min-w-max">
58
+ <div
59
+ className="absolute inset-x-0 bg-surface-hover"
60
+ style={{ top: `calc(${preview.active} * var(--code-line-height))`, height: 'var(--code-line-height)' }}
61
+ />
62
+ <div className="sticky left-0 z-10 w-16 shrink-0 pr-4 text-right font-mono text-(length:--code-font-size) text-text-faint select-none">
63
+ {lines.map((_, index) => (
64
+ <div
65
+ key={index}
66
+ className={`h-(--code-line-height) leading-(--code-line-height) ${index === preview.active ? 'bg-surface-hover text-text' : 'bg-surface'}`}
67
+ >
68
+ {preview.startLine + index + 1}
69
+ </div>
70
+ ))}
71
+ </div>
72
+ <div className="relative">
73
+ {name !== null && (
74
+ <div
75
+ className="absolute rounded-sm bg-(--editor-occurrence) font-mono text-(length:--code-font-size)"
76
+ style={{
77
+ top: `calc(${preview.active} * var(--code-line-height))`,
78
+ height: 'var(--code-line-height)',
79
+ left: `${name.start}ch`,
80
+ width: `${name.end - name.start}ch`
81
+ }}
82
+ />
83
+ )}
84
+ {html?.key === key ? (
85
+ <div className={`relative ${CODE}`} dangerouslySetInnerHTML={{ __html: html.html }} />
86
+ ) : (
87
+ <div className={`relative ${CODE} whitespace-pre`}>{preview.text}</div>
88
+ )}
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ /* The name's stretch of a line in monospace columns, which is what the code is laid out in. */
96
+ function columnsOf(line: string, name: NameRange): NameRange {
97
+ const start = visualColumnOf(line, name.start, TAB_SIZE);
98
+ return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
99
+ }
100
+
101
+ function relativeTo(path: string, folder: string): string {
102
+ return path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
103
+ }
104
+
105
+ function folderOf(path: string): string {
106
+ return path.slice(0, Math.max(0, path.lastIndexOf('/')));
107
+ }
108
+
109
+ /* A line of code with its name in the text color and the rest muted. */
110
+ function PlaceText({ place }: { place: PeekPlace }) {
111
+ if (place.text === '') {
112
+ return <span className="min-w-0 truncate text-text-muted">{`:${place.line + 1}`}</span>;
113
+ }
114
+ const { name } = place;
115
+ if (name === null) {
116
+ return <span className="min-w-0 truncate text-text-muted">{place.text}</span>;
117
+ }
118
+ return (
119
+ <span className="min-w-0 truncate text-text-muted">
120
+ {place.text.slice(0, name.start)}
121
+ <span className="font-semibold text-text">{place.text.slice(name.start, name.end)}</span>
122
+ {place.text.slice(name.end)}
123
+ </span>
124
+ );
125
+ }
126
+
127
+ function Places({ language, view }: { language: EditorLanguage; view: PeekView }) {
128
+ const { t } = useTranslation('editor');
129
+ const activeRow = useRef<HTMLButtonElement>(null);
130
+ const folder = language.project.folder;
131
+ const paths = useMemo(() => view.files.map((file) => relativeTo(fileUriToPath(file.uri) ?? file.uri, folder)), [view.files, folder]);
132
+ const folders = useMemo(() => distinguishingFolders(paths), [paths]);
133
+
134
+ useEffect(() => {
135
+ activeRow.current?.scrollIntoView({ block: 'nearest' });
136
+ }, [view.active]);
137
+
138
+ return (
139
+ <div className="w-1/3 shrink-0 overflow-y-auto border-l border-border py-1">
140
+ {view.files.map((file, fileIndex) => {
141
+ const collapsed = view.collapsed.includes(file.uri);
142
+ return (
143
+ <div key={file.uri}>
144
+ <Tooltip label={paths[fileIndex]!} side="left">
145
+ <div className="flex h-[22px] items-center pr-3 pl-1 text-xs">
146
+ <IconButton
147
+ icon={collapsed ? ChevronRight : ChevronDown}
148
+ size="2xs"
149
+ label={t(collapsed ? 'language.peek.expand' : 'language.peek.collapse')}
150
+ onClick={() => language.peek.toggleFile(file.uri)}
151
+ />
152
+ <FileIcon path={paths[fileIndex]!} size={14} className="ml-0.5 shrink-0" />
153
+ <span className="ml-1 max-w-full shrink-0 truncate">{basenameOf(paths[fileIndex]!)}</span>
154
+ <span className="ml-1.5 shrink-0 text-text-muted">{formatNumber(file.places.length)}</span>
155
+ {folders[fileIndex] !== '' && <span className="ml-2 min-w-0 flex-1 truncate text-text-faint">{folders[fileIndex]}</span>}
156
+ </div>
157
+ </Tooltip>
158
+ {!collapsed &&
159
+ file.places.map((place) => (
160
+ <button
161
+ key={place.id}
162
+ ref={place.id === view.active ? activeRow : undefined}
163
+ type="button"
164
+ data-active={place.id === view.active}
165
+ className="flex h-[22px] w-full items-center pr-3 pl-9 text-left font-mono text-code hover:bg-surface-hover data-[active=true]:bg-accent-soft"
166
+ onClick={() => (place.id === view.active ? language.peek.go(place.id) : language.peek.select(place.id))}
167
+ onDoubleClick={() => language.peek.go(place.id)}
168
+ >
169
+ <PlaceText place={place} />
170
+ </button>
171
+ ))}
172
+ </div>
173
+ );
174
+ })}
175
+ </div>
176
+ );
177
+ }
178
+
179
+ /*
180
+ * The references of a name drawn in the row the editor leaves under the line: the file of the active
181
+ * reference and where it is, the code around it on the left and every place on the right. It lives in a
182
+ * portal into the editor's own row, which the editor makes again when it scrolls back into view. It has
183
+ * the editor's own ground and no edge of its own, so it reads as part of the file.
184
+ */
185
+ export function PeekPanel({ language, view }: { language: EditorLanguage; view: PeekView }) {
186
+ const { t } = useTranslation('editor');
187
+ const active = view.files.flatMap((file) => file.places).find((place) => place.id === view.active);
188
+ const path = active === undefined ? null : fileUriToPath(active.location.uri);
189
+ const folder = language.project.folder;
190
+ const shown = path === null ? '' : folderOf(relativeTo(path, folder));
191
+ if (view.container === null) {
192
+ return null;
193
+ }
194
+ return createPortal(
195
+ <div
196
+ className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
197
+ style={{ height: PEEK_HEIGHT }}
198
+ onPointerDown={(event) => event.preventDefault()}
199
+ >
200
+ <div className="peek-hatch h-[14px]" />
201
+ <div className="flex h-[31px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm">
202
+ <span className="shrink-0 font-medium">{path === null ? '' : basenameOf(path)}</span>
203
+ <PathText path={shown} className="text-text-muted" />
204
+ <div className="ml-auto flex shrink-0 items-center gap-1">
205
+ <span className="text-text-muted">
206
+ {t(view.kind === 'definitions' ? 'language.peek.definitions' : 'language.peek.references', {
207
+ count: view.count,
208
+ formatted: formatNumber(view.count)
209
+ })}
210
+ </span>
211
+ <IconButton icon={X} size="xs" label={t('language.peek.close')} onClick={() => language.peek.close()} />
212
+ </div>
213
+ </div>
214
+ <div className="flex h-[180px] shrink-0">
215
+ <Preview view={view} />
216
+ <Places language={language} view={view} />
217
+ </div>
218
+ <div className="peek-hatch h-[14px]" />
219
+ </div>,
220
+ view.container
221
+ );
222
+ }
@@ -0,0 +1,102 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import type { EditorRect } from '@adecore/editor';
4
+ import type { EditorLanguage } from './editor-language.ts';
5
+ import { PathText } from './PathText.tsx';
6
+ import { placePopup } from './popup-placement.ts';
7
+ import type { PickPreview, PickView } from './popups.ts';
8
+
9
+ const CARD =
10
+ 'fixed top-0 left-0 z-(--z-popup) flex w-[420px] flex-col overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)';
11
+
12
+ function PreviewLines({ preview }: { preview: PickPreview }) {
13
+ return (
14
+ <div className="flex flex-col gap-px border-t border-border p-1 font-mono text-code">
15
+ {preview.removed.map((line, index) => (
16
+ <div key={`-${index}`} className="flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre">
17
+ <span className="text-status-error">−</span>
18
+ <span className="min-w-0 truncate">{line}</span>
19
+ </div>
20
+ ))}
21
+ {preview.added.map((line, index) => (
22
+ <div key={`+${index}`} className="flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre">
23
+ <span className="text-status-idle">+</span>
24
+ <span className="min-w-0 truncate">{line}</span>
25
+ </div>
26
+ ))}
27
+ {preview.note !== null && <div className="px-1.5 font-sans text-xs text-text-muted">{preview.note}</div>}
28
+ {preview.refusal !== undefined && (
29
+ <div role="alert" className="px-1.5 py-0.5 font-sans text-xs text-status-error">
30
+ {preview.refusal}
31
+ </div>
32
+ )}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ /*
38
+ * The list of an editor's pick feature under the character it belongs to. A press on a row never takes
39
+ * the focus from the editor, so the keys keep going to it while the list is open.
40
+ */
41
+ export function PickPopup({ language, view, rect }: { language: EditorLanguage; view: PickView; rect: EditorRect }) {
42
+ const card = useRef<HTMLDivElement>(null);
43
+ const activeRow = useRef<HTMLButtonElement>(null);
44
+
45
+ // Every render: the character moves with the scroll, and the list changes size with its preview.
46
+ useLayoutEffect(() => {
47
+ const element = card.current;
48
+ if (element === null) {
49
+ return;
50
+ }
51
+ const placed = placePopup(
52
+ rect,
53
+ { width: element.offsetWidth, height: element.offsetHeight },
54
+ { width: window.innerWidth, height: window.innerHeight },
55
+ { gap: 2 }
56
+ );
57
+ element.style.left = `${Math.round(placed.left)}px`;
58
+ element.style.top = `${Math.round(placed.top)}px`;
59
+ element.style.maxHeight = `${Math.floor(placed.maxHeight)}px`;
60
+ element.style.visibility = 'visible';
61
+ });
62
+
63
+ useLayoutEffect(() => {
64
+ activeRow.current?.scrollIntoView({ block: 'nearest' });
65
+ }, [view.active]);
66
+
67
+ return createPortal(
68
+ <div ref={card} className={CARD} style={{ visibility: 'hidden' }} onPointerDown={(event) => event.preventDefault()}>
69
+ {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">
71
+ {view.groups.map((group, groupIndex) => (
72
+ <div key={group.title ?? groupIndex} role="group" className="flex flex-col">
73
+ {group.title !== null && <div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint">{group.title}</div>}
74
+ {group.rows.map((row) => (
75
+ <button
76
+ key={row.id}
77
+ ref={row.id === view.active ? activeRow : undefined}
78
+ type="button"
79
+ role="option"
80
+ aria-selected={row.id === view.active}
81
+ data-active={row.id === view.active}
82
+ className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
83
+ onClick={() => language.pick.choose(row.id)}
84
+ >
85
+ <span className={row.path ? 'shrink-0' : 'min-w-0 truncate'}>{row.label}</span>
86
+ {row.path ? (
87
+ <PathText path={row.detail} className="ml-auto font-mono text-text-faint" />
88
+ ) : (
89
+ <span className="ml-auto shrink-0 font-mono text-text-faint">
90
+ {row.id === view.active && row.detail === '' ? '↵' : row.detail}
91
+ </span>
92
+ )}
93
+ </button>
94
+ ))}
95
+ </div>
96
+ ))}
97
+ </div>
98
+ {view.preview !== null && <PreviewLines preview={view.preview} />}
99
+ </div>,
100
+ document.body
101
+ );
102
+ }
@@ -0,0 +1,219 @@
1
+ import { useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Sparkles } from 'lucide-react';
5
+ import { fileUriToPath } from '@adecore/lsp';
6
+ import type { EditorRect } from '@adecore/editor';
7
+ import { Button, Icon } from '@adecore/ui';
8
+ import { formatNumber } from '@adecore/ui/format';
9
+ import { basenameOf } from './paths.ts';
10
+ import { AnchoredPopup } from './AnchoredPopup.tsx';
11
+ import type { EditorLanguage } from './editor-language.ts';
12
+ import type { RenameView } from './popups.ts';
13
+
14
+ const CARD_ATTRIBUTE = 'data-rename-card';
15
+
16
+ /* Whether the focus moved to something that belongs to the rename, which is not leaving it. */
17
+ function staysInside(target: EventTarget | null): boolean {
18
+ return target instanceof Element && target.closest(`[${CARD_ATTRIBUTE}]`) !== null;
19
+ }
20
+
21
+ /*
22
+ * Suggested names from the host, in a list under the input; a press fills the input and the
23
+ * typed name stays what Enter applies. While it is up the card says only its two keys.
24
+ */
25
+ function Suggestions({ names, active, onPick }: { names: readonly string[]; active: number; onPick(name: string): void }) {
26
+ const { t } = useTranslation('editor');
27
+ return (
28
+ <div className="flex w-[250px] flex-col p-1.25">
29
+ <div className="flex items-center gap-1.5 px-2.25 pt-1.25 pb-1 text-xs font-semibold text-text-faint">
30
+ <Icon icon={Sparkles} size={12} className="shrink-0" />
31
+ {t('language.rename.suggestions')}
32
+ </div>
33
+ {names.map((name, index) => (
34
+ <button
35
+ key={name}
36
+ type="button"
37
+ tabIndex={-1}
38
+ className={`flex h-6.5 items-center gap-2 rounded-md px-2.25 text-left font-mono text-code text-text ${index === active ? 'bg-accent/20' : 'hover:bg-surface-hover'}`}
39
+ onClick={() => onPick(name)}
40
+ >
41
+ <Icon icon={Sparkles} size={12} className={`shrink-0 ${index === active ? 'text-accent' : 'text-text-faint'}`} />
42
+ {name}
43
+ </button>
44
+ ))}
45
+ <div className="mt-1 flex gap-3 border-t border-border px-2.25 pt-1.75 pb-0.75 text-xs text-text-muted">
46
+ <span>↵ {t('language.rename.rename')}</span>
47
+ <span>⇥ {t('language.rename.useSuggestion')}</span>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function Hints({ view, active, onPick }: { view: RenameView; active: number; onPick(name: string): void }) {
54
+ const { t } = useTranslation('editor');
55
+ const { occurrences } = view;
56
+ return (
57
+ <div className="flex flex-col">
58
+ {view.error !== null && (
59
+ <div role="alert" className="max-w-[420px] px-2.5 py-1.5 text-xs text-status-error">
60
+ {view.error}
61
+ </div>
62
+ )}
63
+ {view.suggestions.length > 0 ? (
64
+ <Suggestions names={view.suggestions} active={active} onPick={onPick} />
65
+ ) : (
66
+ <div className="flex items-center gap-3 px-2.5 py-1.5 text-xs whitespace-nowrap text-text-muted">
67
+ {occurrences !== null && (
68
+ <span>
69
+ {occurrences.files > 1
70
+ ? t('language.rename.occurrencesFiles', {
71
+ count: occurrences.count,
72
+ formatted: formatNumber(occurrences.count),
73
+ files: formatNumber(occurrences.files)
74
+ })
75
+ : t('language.rename.occurrences', { count: occurrences.count, formatted: formatNumber(occurrences.count) })}
76
+ </span>
77
+ )}
78
+ <span>↵ {t('language.rename.rename')}</span>
79
+ <span>⇧↵ {t('language.rename.preview')}</span>
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function Preview({ language, view }: { language: EditorLanguage; view: RenameView }) {
87
+ const { t } = useTranslation('editor');
88
+ const folder = language.project.folder;
89
+ return (
90
+ <div className="flex w-[520px] max-w-full flex-col" onPointerDown={(event) => event.preventDefault()}>
91
+ <div className="max-h-[320px] overflow-y-auto">
92
+ {view.files.map((file) => {
93
+ const path = fileUriToPath(file.uri);
94
+ const shown = path === null ? file.uri : path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
95
+ return (
96
+ <div key={file.uri} className="border-b border-border last:border-b-0">
97
+ <div className="flex items-center gap-2 px-3 py-1.5 text-xs">
98
+ <span className="font-medium">{path === null ? shown : basenameOf(path)}</span>
99
+ <span className="min-w-0 truncate text-text-faint">{shown}</span>
100
+ </div>
101
+ <div className="flex flex-col gap-px px-1 pb-1.5 font-mono text-code">
102
+ {file.rows.map((row) => (
103
+ <div key={row.line} className="flex flex-col gap-px">
104
+ <div className="flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre">
105
+ <span className="w-8 shrink-0 text-right text-text-faint">{row.line}</span>
106
+ <span className="min-w-0 truncate">{row.before}</span>
107
+ </div>
108
+ <div className="flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre">
109
+ <span className="w-8 shrink-0" />
110
+ <span className="min-w-0 truncate">{row.after}</span>
111
+ </div>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ );
117
+ })}
118
+ </div>
119
+ <div className="flex items-center justify-end gap-2 border-t border-border p-2">
120
+ <Button size="xs" variant="ghost" onClick={() => language.rename.cancel()}>
121
+ {t('language.rename.cancel')}
122
+ </Button>
123
+ <Button size="xs" variant="primary" onClick={() => void language.rename.confirm()}>
124
+ {t('language.rename.apply')}
125
+ </Button>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ /*
132
+ * The rename of a symbol: an input over the symbol itself, and under it either the count of its places with
133
+ * the keys, or after Shift+Enter the lines the rename would change. The input keeps the focus the whole time,
134
+ * so Enter and Escape mean the same in both, and the focus leaving the card means the person went elsewhere.
135
+ */
136
+ export function RenameCard({ language, view, rect, endRect }: { language: EditorLanguage; view: RenameView; rect: EditorRect; endRect: EditorRect | null }) {
137
+ const input = useRef<HTMLInputElement>(null);
138
+ const [value, setValue] = useState(view.placeholder);
139
+ const [active, setActive] = useState(0);
140
+
141
+ useLayoutEffect(() => {
142
+ input.current?.focus();
143
+ input.current?.select();
144
+ }, []);
145
+
146
+ function fill(name: string): void {
147
+ setValue(name);
148
+ language.rename.edited();
149
+ input.current?.focus();
150
+ }
151
+
152
+ function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
153
+ const names = view.phase === 'input' ? view.suggestions : [];
154
+ if (names.length > 0 && !event.nativeEvent.isComposing && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
155
+ const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
156
+ if (step !== 0 || event.key === 'Tab') {
157
+ event.preventDefault();
158
+ event.stopPropagation();
159
+ if (event.key === 'Tab') {
160
+ fill(names[Math.min(active, names.length - 1)]!);
161
+ } else {
162
+ setActive((active + step + names.length) % names.length);
163
+ }
164
+ return;
165
+ }
166
+ }
167
+ if (event.key === 'Escape') {
168
+ event.preventDefault();
169
+ event.stopPropagation();
170
+ language.rename.cancel();
171
+ } else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
172
+ event.preventDefault();
173
+ event.stopPropagation();
174
+ if (view.phase === 'preview') {
175
+ void language.rename.confirm();
176
+ } else if (!view.busy) {
177
+ void language.rename.submit(value, event.shiftKey);
178
+ }
179
+ }
180
+ }
181
+
182
+ const symbolWidth = endRect === null ? 0 : Math.max(0, endRect.left - rect.left);
183
+ return createPortal(
184
+ <>
185
+ <input
186
+ ref={input}
187
+ {...{ [CARD_ATTRIBUTE]: '' }}
188
+ value={view.phase === 'preview' ? view.name : value}
189
+ readOnly={view.phase === 'preview' || view.busy}
190
+ spellCheck={false}
191
+ autoComplete="off"
192
+ aria-label={view.original}
193
+ className="fixed z-(--z-popup) rounded-sm border border-accent bg-surface px-1 font-mono text-code text-text shadow-(--float-shadow) outline-none [field-sizing:content]"
194
+ style={{
195
+ left: Math.round(rect.left) - 5,
196
+ top: Math.round(rect.top) - 2,
197
+ height: Math.round(rect.bottom - rect.top) + 4,
198
+ minWidth: Math.round(symbolWidth) + 10
199
+ }}
200
+ onChange={(event) => {
201
+ setValue(event.target.value);
202
+ language.rename.edited();
203
+ }}
204
+ onKeyDown={onKeyDown}
205
+ onBlur={(event) => {
206
+ if (!staysInside(event.relatedTarget)) {
207
+ language.rename.cancel();
208
+ }
209
+ }}
210
+ />
211
+ <AnchoredPopup rect={{ ...rect, top: rect.top - 2, bottom: rect.bottom + 2 }} className="overflow-hidden" placement={{ gap: 2 }}>
212
+ <div {...{ [CARD_ATTRIBUTE]: '' }} onPointerDown={(event) => event.preventDefault()}>
213
+ {view.phase === 'preview' ? <Preview language={language} view={view} /> : <Hints view={view} active={active} onPick={fill} />}
214
+ </div>
215
+ </AnchoredPopup>
216
+ </>,
217
+ document.body
218
+ );
219
+ }