@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,42 @@
1
+ import { Markdown } from './Markdown.tsx';
2
+ import type { SignatureViewModel } from './signature-model.ts';
3
+
4
+ /* The signature with the parameter being typed underlined, and what is said about that parameter below it. */
5
+ export function SignatureCard({ model }: { model: SignatureViewModel }) {
6
+ const { label, active } = model;
7
+ const documentation = model.parameterDocumentation !== '' ? model.parameterDocumentation : model.documentation;
8
+
9
+ return (
10
+ <div className="flex w-max max-w-[min(560px,calc(100vw-16px))] min-w-[240px] flex-col divide-y divide-border">
11
+ <div className="px-3 py-2 font-mono text-code break-words whitespace-pre-wrap text-text-muted">
12
+ {active === null ? (
13
+ label
14
+ ) : (
15
+ <>
16
+ {label.slice(0, active.start)}
17
+ <span className="font-semibold text-text underline decoration-2 underline-offset-2">{label.slice(active.start, active.end)}</span>
18
+ {label.slice(active.end)}
19
+ </>
20
+ )}
21
+ {model.count > 1 && (
22
+ <span className="ml-2 font-sans text-xs text-text-faint">
23
+ {model.index + 1}/{model.count}
24
+ </span>
25
+ )}
26
+ </div>
27
+ {documentation !== '' && (
28
+ <div className="flex items-baseline gap-1.5 px-3 py-2 text-xs text-text-muted select-text [&_.adecore-editor-markdown]:text-xs [&_.adecore-editor-markdown_p]:m-0">
29
+ {model.parameterName !== '' && model.parameterDocumentation !== '' && (
30
+ <>
31
+ <span className="shrink-0 font-semibold text-text">{model.parameterName}</span>
32
+ <span aria-hidden>·</span>
33
+ </>
34
+ )}
35
+ <div className="min-w-0">
36
+ <Markdown text={documentation} />
37
+ </div>
38
+ </div>
39
+ )}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,219 @@
1
+ import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { AtSign, Hash } from 'lucide-react';
5
+ import { Icon } from '@adecore/ui';
6
+ import { formatShortcut } from '@adecore/ui';
7
+ import { formatNumber } from '@adecore/ui/format';
8
+ import { basenameOf } from './paths.ts';
9
+ import { fileUriToPath } from '@adecore/lsp';
10
+ import type { EditorLanguage } from './editor-language.ts';
11
+ import type { SymbolsView } from './popups.ts';
12
+ import {
13
+ filterEntries,
14
+ groupEntries,
15
+ letterOfKind,
16
+ modeOf,
17
+ toneOfKind,
18
+ type SymbolEntry,
19
+ type SymbolTone,
20
+ type WorkspaceEntry
21
+ } from './symbol-picker-model.ts';
22
+
23
+ const TONES: Record<SymbolTone, string> = {
24
+ callable: 'bg-accent-soft text-accent',
25
+ value: 'bg-status-running/15 text-status-running',
26
+ type: 'bg-status-idle/15 text-status-idle',
27
+ other: 'bg-surface-hover text-text-muted'
28
+ };
29
+
30
+ const SEARCH_DELAY_MS = 200;
31
+
32
+ function Badge({ kind }: { kind: number }) {
33
+ return (
34
+ <span className={`grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONES[toneOfKind(kind)]}`}>
35
+ {letterOfKind(kind)}
36
+ </span>
37
+ );
38
+ }
39
+
40
+ /*
41
+ * The picker of go to symbol, over the editor: an input, the symbols of the file in groups by kind, and the
42
+ * keys. The input keeps the focus; Enter goes, Escape and the focus leaving close it.
43
+ */
44
+ export function SymbolPicker({ language, view }: { language: EditorLanguage; view: SymbolsView }) {
45
+ const { t } = useTranslation('editor');
46
+ const [input, setInput] = useState('');
47
+ const [active, setActive] = useState(0);
48
+ const [found, setFound] = useState<{ text: string; entries: WorkspaceEntry[] }>({ text: '', entries: [] });
49
+ const activeRow = useRef<HTMLButtonElement>(null);
50
+ const mode = modeOf(input);
51
+ const filterText = mode.mode === 'symbol' ? mode.text : '';
52
+ const searchText = mode.mode === 'workspace' ? mode.text : '';
53
+
54
+ const groups = useMemo(() => groupEntries(filterEntries(view.entries, filterText)), [view.entries, filterText]);
55
+ const flat: SymbolEntry[] = groups.flatMap((group) => group.entries);
56
+ const places = mode.mode === 'workspace' && found.text === mode.text ? found.entries : [];
57
+ const count = mode.mode === 'workspace' ? places.length : mode.mode === 'line' ? (mode.line === null ? 0 : 1) : flat.length;
58
+
59
+ useEffect(() => {
60
+ if (searchText === '') {
61
+ return;
62
+ }
63
+ const controller = new AbortController();
64
+ const text = searchText;
65
+ const timer = setTimeout(() => {
66
+ language.symbolPicker
67
+ .search(text, controller.signal)
68
+ .then((entries) => !controller.signal.aborted && setFound({ text, entries }))
69
+ .catch(() => undefined);
70
+ }, SEARCH_DELAY_MS);
71
+ return () => {
72
+ clearTimeout(timer);
73
+ controller.abort();
74
+ };
75
+ }, [language, searchText]);
76
+
77
+ useEffect(() => {
78
+ activeRow.current?.scrollIntoView({ block: 'nearest' });
79
+ }, [active, input]);
80
+
81
+ function choose(index: number): void {
82
+ if (mode.mode === 'line') {
83
+ if (mode.line !== null) {
84
+ language.symbolPicker.goToLine(mode.line);
85
+ }
86
+ } else if (mode.mode === 'workspace') {
87
+ const place = places[index];
88
+ if (place !== undefined) {
89
+ language.symbolPicker.goToWorkspace(place);
90
+ }
91
+ } else if (flat[index] !== undefined) {
92
+ language.symbolPicker.goTo(flat[index]!);
93
+ }
94
+ }
95
+
96
+ function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
97
+ if (event.key === 'Escape') {
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ language.symbolPicker.close();
101
+ } else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
102
+ event.preventDefault();
103
+ event.stopPropagation();
104
+ choose(active);
105
+ } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
106
+ event.preventDefault();
107
+ setActive(Math.max(0, Math.min(Math.max(0, count - 1), active + (event.key === 'ArrowDown' ? 1 : -1))));
108
+ }
109
+ }
110
+
111
+ let offset = 0;
112
+ return createPortal(
113
+ <div
114
+ className="fixed top-[12vh] left-1/2 z-(--z-popup) flex w-[560px] max-w-[calc(100vw-16px)] -translate-x-1/2 flex-col overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)"
115
+ onPointerDown={(event) => event.target !== event.currentTarget && (event.target as HTMLElement).tagName !== 'INPUT' && event.preventDefault()}
116
+ >
117
+ <div className="flex items-center gap-2 border-b border-border px-3 py-2">
118
+ <Icon icon={mode.mode === 'workspace' ? Hash : AtSign} size={14} className="shrink-0 text-text-muted" />
119
+ <input
120
+ autoFocus
121
+ value={input}
122
+ spellCheck={false}
123
+ autoComplete="off"
124
+ placeholder={t('language.symbols.placeholder', { file: view.file })}
125
+ className="min-w-0 flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-faint"
126
+ onChange={(event) => {
127
+ setInput(event.target.value);
128
+ setActive(0);
129
+ }}
130
+ onKeyDown={onKeyDown}
131
+ onBlur={() => language.symbolPicker.close()}
132
+ />
133
+ <span className="shrink-0 text-xs text-text-faint">
134
+ {language.shortcuts.goToSymbol === null ? '' : formatShortcut(language.shortcuts.goToSymbol, language.apple)}
135
+ </span>
136
+ </div>
137
+ <div role="listbox" className="max-h-[320px] overflow-y-auto p-1">
138
+ {mode.mode === 'line' && (
139
+ <div className="px-2 py-1.5 text-xs text-text-muted">
140
+ {mode.line === null ? t('language.symbols.lineHint') : t('language.symbols.goToLine', { line: formatNumber(mode.line) })}
141
+ </div>
142
+ )}
143
+ {mode.mode === 'workspace' &&
144
+ places.map((place, index) => {
145
+ const path = fileUriToPath(place.uri);
146
+ return (
147
+ <button
148
+ key={place.id}
149
+ ref={index === active ? activeRow : undefined}
150
+ type="button"
151
+ role="option"
152
+ aria-selected={index === active}
153
+ data-active={index === active}
154
+ className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
155
+ onClick={() => choose(index)}
156
+ >
157
+ <Badge kind={place.kind} />
158
+ <span className="shrink-0">{place.name}</span>
159
+ <span className="min-w-0 truncate font-mono text-text-faint">{place.container}</span>
160
+ <span className="ml-auto shrink-0 font-mono text-text-faint">
161
+ {path === null ? '' : basenameOf(path)}:{formatNumber(place.line + 1)}
162
+ </span>
163
+ </button>
164
+ );
165
+ })}
166
+ {mode.mode === 'symbol' &&
167
+ groups.map((group) => {
168
+ const start = offset;
169
+ offset += group.entries.length;
170
+ return (
171
+ <div key={group.group} role="group">
172
+ <div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint">{t(`language.symbols.groups.${group.group}`)}</div>
173
+ {group.entries.map((entry, index) => {
174
+ const position = start + index;
175
+ return (
176
+ <button
177
+ key={entry.id}
178
+ ref={position === active ? activeRow : undefined}
179
+ type="button"
180
+ role="option"
181
+ aria-selected={position === active}
182
+ data-active={position === active}
183
+ className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
184
+ onClick={() => choose(position)}
185
+ >
186
+ <Badge kind={entry.kind} />
187
+ <span className="shrink-0">{entry.name}</span>
188
+ <span className="min-w-0 truncate font-mono text-text-faint">{entry.detail}</span>
189
+ {entry.container !== '' && <span className="min-w-0 truncate text-text-faint">{entry.container}</span>}
190
+ <span className="ml-auto shrink-0 font-mono text-text-faint">{formatNumber(entry.line + 1)}</span>
191
+ </button>
192
+ );
193
+ })}
194
+ </div>
195
+ );
196
+ })}
197
+ {mode.mode === 'symbol' && flat.length === 0 && (
198
+ <div className="px-2 py-3 text-center text-xs text-text-muted">{t('language.symbols.none')}</div>
199
+ )}
200
+ {mode.mode === 'workspace' && places.length === 0 && (
201
+ <div className="px-2 py-3 text-center text-xs text-text-muted">
202
+ {mode.text === '' ? t('language.symbols.workspaceHint') : t('language.symbols.none')}
203
+ </div>
204
+ )}
205
+ </div>
206
+ <div className="flex items-center gap-3 border-t border-border px-3 py-1.5 text-xs whitespace-nowrap text-text-faint">
207
+ <span>↑↓ {t('language.symbols.navigate')}</span>
208
+ <span>↵ {t('language.symbols.open')}</span>
209
+ <button type="button" className="ml-auto hover:text-text" onClick={() => setInput('#')}>
210
+ # {t('language.symbols.workspace')}
211
+ </button>
212
+ <button type="button" className="hover:text-text" onClick={() => setInput(':')}>
213
+ : {t('language.symbols.line')}
214
+ </button>
215
+ </div>
216
+ </div>,
217
+ document.body
218
+ );
219
+ }
@@ -0,0 +1,146 @@
1
+ import { applyTextEdits, type CodeAction, type Command, type Diagnostic, type Range, type WorkspaceEdit } from '@adecore/lsp';
2
+ import { comparePositions } from './diagnostics-model.ts';
3
+ import { entriesOf, renamesOf } from './workspace-edit.ts';
4
+
5
+ export type ActionGroup = 'quickfix' | 'rewrite' | 'extract' | 'inline' | 'move' | 'refactor' | 'source' | 'other';
6
+
7
+ /* The order of the list under the caret: the fixes and the rewrites that are one keystroke away first, the refactorings after them. */
8
+ export const ACTION_GROUPS: readonly ActionGroup[] = ['quickfix', 'rewrite', 'extract', 'inline', 'move', 'refactor', 'source', 'other'];
9
+
10
+ /* The order of Refactor This: what changes the structure of the code first, the rewrites after it. */
11
+ export const REFACTOR_GROUPS: readonly ActionGroup[] = ['extract', 'inline', 'move', 'rewrite', 'refactor'];
12
+
13
+ /* An action of a server with the id its row has in the list. */
14
+ export interface ActionEntry {
15
+ readonly id: string;
16
+ readonly action: CodeAction;
17
+ readonly group: ActionGroup;
18
+ }
19
+
20
+ /* The kinds nest by dots: `refactor.extract.function` is an extraction and `source.organizeImports` a source action. */
21
+ export function groupOf(kind: string | undefined): ActionGroup {
22
+ if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
23
+ return 'quickfix';
24
+ }
25
+ for (const group of ['extract', 'inline', 'move', 'rewrite'] as const) {
26
+ if (kind === `refactor.${group}` || kind?.startsWith(`refactor.${group}.`)) {
27
+ return group;
28
+ }
29
+ }
30
+ if (kind === 'refactor' || kind?.startsWith('refactor.')) {
31
+ return 'refactor';
32
+ }
33
+ if (kind === 'source' || kind?.startsWith('source.')) {
34
+ return 'source';
35
+ }
36
+ return 'other';
37
+ }
38
+
39
+ /*
40
+ * What a server answered, as the rows of a list. A bare command becomes an action of its own, one a server
41
+ * marked disabled is left out, and each group lists the preferred actions first, in the order the server gave.
42
+ */
43
+ export function actionsOf(result: readonly (CodeAction | Command)[] | null, order: readonly ActionGroup[] = ACTION_GROUPS): ActionEntry[] {
44
+ const entries = (result ?? [])
45
+ .map((item) => (typeof item.command === 'string' ? ({ title: item.title, command: item as Command } satisfies CodeAction) : (item as CodeAction)))
46
+ .filter((action) => action.disabled === undefined)
47
+ .map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
48
+ return order.flatMap((group) => {
49
+ const own = entries.filter((entry) => entry.group === group);
50
+ return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
51
+ });
52
+ }
53
+
54
+ /* Entries from several requests as one list: the same order as a single answer, and an id of its own for each row. Two actions with a title and kind alike are one. */
55
+ export function mergeEntries(lists: readonly (readonly ActionEntry[])[]): ActionEntry[] {
56
+ const seen = new Set<string>();
57
+ const unique = lists.flat().filter((entry) => {
58
+ const key = `${entry.action.kind ?? ''}\0${entry.action.title}`;
59
+ if (seen.has(key)) {
60
+ return false;
61
+ }
62
+ seen.add(key);
63
+ return true;
64
+ });
65
+ return ACTION_GROUPS.flatMap((group) => {
66
+ const own = unique.filter((entry) => entry.group === group);
67
+ return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
68
+ }).map((entry, index) => ({ ...entry, id: String(index) }));
69
+ }
70
+
71
+ /* Whether the lightbulb offers it: a source action such as organizing imports is a command of its own and never a hint. */
72
+ export function isHint(entry: ActionEntry): boolean {
73
+ return entry.group !== 'source';
74
+ }
75
+
76
+ /* The problems of a line that are worth a fix: errors and warnings, errors first, in the order of the line. */
77
+ export function fixableOnLine(diagnostics: readonly Diagnostic[], line: number): Diagnostic[] {
78
+ return diagnostics
79
+ .filter((diagnostic) => (diagnostic.severity ?? 1) <= 2 && diagnostic.range.start.line <= line && diagnostic.range.end.line >= line)
80
+ .sort((left, right) => (left.severity ?? 1) - (right.severity ?? 1) || comparePositions(left.range.start, right.range.start));
81
+ }
82
+
83
+ function touches(range: Range, target: Range): boolean {
84
+ return comparePositions(range.start, target.end) <= 0 && comparePositions(target.start, range.end) <= 0;
85
+ }
86
+
87
+ /* The problems an action may be about: the ones that overlap or touch the range it was asked for. */
88
+ export function diagnosticsAt(diagnostics: readonly Diagnostic[], range: Range): Diagnostic[] {
89
+ return diagnostics.filter((diagnostic) => touches(diagnostic.range, range));
90
+ }
91
+
92
+ /* What an edit would change in the open file, as the lines it takes away and the lines that stand in their place. */
93
+ export interface EditPreview {
94
+ readonly removed: readonly string[];
95
+ readonly added: readonly string[];
96
+ /* Lines left out of `removed` and `added` to keep the preview short. */
97
+ readonly hiddenLines: number;
98
+ /* The other files the edit reaches. */
99
+ readonly otherFiles: number;
100
+ /* The files it moves. */
101
+ readonly moves: number;
102
+ }
103
+
104
+ const PREVIEW_LINES = 3;
105
+
106
+ function linesOf(text: string): string[] {
107
+ return text.split(/\r\n|\r|\n/);
108
+ }
109
+
110
+ /* Null when the edit changes nothing, or creates or deletes a file, which a preview of lines cannot show. */
111
+ export function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditPreview | null {
112
+ const entries = entriesOf(edit);
113
+ if (entries === null) {
114
+ return null;
115
+ }
116
+ const own = entries.get(uri);
117
+ const moves = renamesOf(edit).length;
118
+ const otherFiles = [...entries.keys()].filter((key) => key !== uri).length;
119
+ let removed: string[] = [];
120
+ let added: string[] = [];
121
+ if (own !== undefined) {
122
+ let after: string;
123
+ try {
124
+ after = applyTextEdits(text, own.flat());
125
+ } catch {
126
+ return null;
127
+ }
128
+ const before = linesOf(text);
129
+ const next = linesOf(after);
130
+ let head = 0;
131
+ while (head < before.length && head < next.length && before[head] === next[head]) {
132
+ head++;
133
+ }
134
+ let tail = 0;
135
+ while (tail < before.length - head && tail < next.length - head && before[before.length - 1 - tail] === next[next.length - 1 - tail]) {
136
+ tail++;
137
+ }
138
+ removed = before.slice(head, before.length - tail);
139
+ added = next.slice(head, next.length - tail);
140
+ }
141
+ if (removed.length === 0 && added.length === 0 && otherFiles === 0 && moves === 0) {
142
+ return null;
143
+ }
144
+ const hiddenLines = Math.max(0, removed.length - PREVIEW_LINES) + Math.max(0, added.length - PREVIEW_LINES);
145
+ return { removed: removed.slice(0, PREVIEW_LINES), added: added.slice(0, PREVIEW_LINES), hiddenLines, otherFiles, moves };
146
+ }