@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,440 @@
1
+ import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import type { CompletionItem, CompletionList, CompletionResult, InsertReplaceEdit, MarkupContent, TextEdit } from '@adecore/lsp';
3
+ import { comparePositions } from './diagnostics-model.ts';
4
+ import { isEmptyHover, splitSignatures, type HoverText } from './hover-content.ts';
5
+ import type { CompletionDocs } from './popups.ts';
6
+ import { parseSnippet, tabOrder, type SnippetStop } from './snippet.ts';
7
+
8
+ const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
9
+
10
+ export function isIdentifierCharacter(character: string): boolean {
11
+ return IDENTIFIER_CHARACTER.test(character);
12
+ }
13
+
14
+ let lastBefore = '';
15
+ let lastPrefix = '';
16
+
17
+ /* The word being typed: the identifier characters at the end of the text before the caret. A list asks it of every item with the same line, so the last answer is kept. */
18
+ export function identifierPrefix(before: string): string {
19
+ if (before === lastBefore) {
20
+ return lastPrefix;
21
+ }
22
+ const characters = Array.from(before);
23
+ let start = characters.length;
24
+ while (start > 0 && IDENTIFIER_CHARACTER.test(characters[start - 1]!)) {
25
+ start--;
26
+ }
27
+ lastBefore = before;
28
+ lastPrefix = characters.slice(start).join('');
29
+ return lastPrefix;
30
+ }
31
+
32
+ export interface MatchDetail {
33
+ /* Lower is better. */
34
+ readonly score: number;
35
+ /* The UTF-16 offsets of the characters of the value the prefix matched, for marking them. */
36
+ readonly positions: readonly number[];
37
+ }
38
+
39
+ function run(start: number, length: number): number[] {
40
+ return Array.from({ length }, (_, offset) => start + offset);
41
+ }
42
+
43
+ interface LabelShape {
44
+ readonly lower: string;
45
+ /* Where each word of the label starts: after an underscore, a dollar sign, a space or a hyphen, and at each capital. */
46
+ readonly offsets: readonly number[];
47
+ /* The lowercased first letter of each word. */
48
+ readonly initials: string;
49
+ }
50
+
51
+ const SHAPE_LIMIT = 4000;
52
+ const shapes = new Map<string, LabelShape>();
53
+
54
+ /* What matching reads of a label, which is the same at every keystroke of a word and costs a regular expression pass to find. */
55
+ function shapeOf(value: string): LabelShape {
56
+ const known = shapes.get(value);
57
+ if (known !== undefined) {
58
+ return known;
59
+ }
60
+ const boundaries = [...value.matchAll(/(?:^|[_$\s-])([\p{L}\p{N}])|(\p{Lu})/gu)];
61
+ const shape: LabelShape = {
62
+ lower: value.toLocaleLowerCase(),
63
+ offsets: boundaries.map((boundary) => boundary.index! + (boundary[1] ? boundary[0].length - boundary[1].length : 0)),
64
+ initials: boundaries
65
+ .map((match) => match[1] ?? match[2])
66
+ .join('')
67
+ .toLocaleLowerCase()
68
+ };
69
+ if (shapes.size >= SHAPE_LIMIT) {
70
+ shapes.clear();
71
+ }
72
+ shapes.set(value, shape);
73
+ return shape;
74
+ }
75
+
76
+ /*
77
+ * How well a prefix fits a label, lower being better, and which characters it matched, or undefined when
78
+ * it does not fit. A match that starts the label beats one that ignores case, which beats one at a camel
79
+ * hump or after an underscore, then the initials of the humps (`gbi` for `getById`), and last the letters
80
+ * in order for a prefix of two or more.
81
+ */
82
+ export function matchDetail(value: string, prefix: string): MatchDetail | undefined {
83
+ if (prefix === '') {
84
+ return { score: 0, positions: [] };
85
+ }
86
+ if (value.startsWith(prefix)) {
87
+ return { score: 0, positions: run(0, prefix.length) };
88
+ }
89
+ const { lower, offsets, initials } = shapeOf(value);
90
+ const query = prefix.toLocaleLowerCase();
91
+ if (lower.startsWith(query)) {
92
+ return { score: 1, positions: run(0, query.length) };
93
+ }
94
+ for (const offset of offsets) {
95
+ if (lower.slice(offset).startsWith(query)) {
96
+ return { score: 2 + offset / 1000, positions: run(offset, query.length) };
97
+ }
98
+ }
99
+ if (initials.startsWith(query)) {
100
+ return { score: 3, positions: offsets.slice(0, query.length) };
101
+ }
102
+ if (query.length < 2 || lower[0] !== query[0]) {
103
+ return undefined;
104
+ }
105
+ let cursor = 0;
106
+ const positions: number[] = [];
107
+ for (const character of query) {
108
+ const next = lower.indexOf(character, cursor);
109
+ if (next < 0) {
110
+ return undefined;
111
+ }
112
+ positions.push(next);
113
+ cursor = next + character.length;
114
+ }
115
+ return { score: 4 + cursor / 1000, positions };
116
+ }
117
+
118
+ export function matchScore(value: string, prefix: string): number | undefined {
119
+ return matchDetail(value, prefix)?.score;
120
+ }
121
+
122
+ /* The range an item replaces when it is accepted; for an insert-or-replace edit, the one `replace` asks for. */
123
+ export function editRangeOf(item: CompletionItem, replace: boolean): EditorRange | null {
124
+ const edit = item.textEdit;
125
+ if (edit === undefined) {
126
+ return null;
127
+ }
128
+ if ('range' in edit) {
129
+ return edit.range;
130
+ }
131
+ return replace ? edit.replace : edit.insert;
132
+ }
133
+
134
+ /* Items from a result, with the list's defaults filled in so each carries its own edit range and format. */
135
+ export function itemsOf(result: CompletionResult): { items: CompletionItem[]; incomplete: boolean } {
136
+ if (result === null) {
137
+ return { items: [], incomplete: false };
138
+ }
139
+ if (Array.isArray(result)) {
140
+ return { items: result, incomplete: false };
141
+ }
142
+ const list: CompletionList = result;
143
+ const defaults = list.itemDefaults;
144
+ if (defaults === undefined) {
145
+ return { items: list.items, incomplete: list.isIncomplete };
146
+ }
147
+ return {
148
+ incomplete: list.isIncomplete,
149
+ items: list.items.map((item) => {
150
+ const next: CompletionItem = { ...item };
151
+ if (defaults.insertTextFormat !== undefined && next.insertTextFormat === undefined) {
152
+ next.insertTextFormat = defaults.insertTextFormat;
153
+ }
154
+ if (defaults.commitCharacters !== undefined && next.commitCharacters === undefined) {
155
+ next.commitCharacters = defaults.commitCharacters;
156
+ }
157
+ if (next.textEdit === undefined && defaults.editRange !== undefined) {
158
+ const text = item.textEditText ?? item.insertText ?? item.label;
159
+ next.textEdit =
160
+ 'start' in defaults.editRange
161
+ ? { range: defaults.editRange, newText: text }
162
+ : { insert: defaults.editRange.insert, replace: defaults.editRange.replace, newText: text };
163
+ }
164
+ return next;
165
+ })
166
+ };
167
+ }
168
+
169
+ /*
170
+ * What a person has typed of an item: from the start of the range it will replace up to the caret when
171
+ * the caret is in that range, else the word before the caret. Matching against that is what keeps
172
+ * `thisArg.` from filtering on the dot.
173
+ */
174
+ export function prefixFor(item: CompletionItem, caret: EditorPosition, lineBefore: string, textBetween: (range: EditorRange) => string): string {
175
+ const range = editRangeOf(item, false);
176
+ if (range !== null && range.start.line === caret.line && comparePositions(range.start, caret) <= 0 && comparePositions(caret, range.end) <= 0) {
177
+ return textBetween({ start: range.start, end: caret });
178
+ }
179
+ return identifierPrefix(lineBefore);
180
+ }
181
+
182
+ export interface Ranked {
183
+ readonly item: CompletionItem;
184
+ readonly score: number;
185
+ /* What was typed of the item, which its matched characters are found against. */
186
+ readonly prefix: string;
187
+ }
188
+
189
+ /*
190
+ * The items that fit what is typed, best first: by how they match, then one the person chose lately, a
191
+ * preselected one, and the server's own order. `recency` is higher for a choice made more recently, and 0 for none.
192
+ */
193
+ export function rankCompletions(
194
+ items: readonly CompletionItem[],
195
+ prefixOf: (item: CompletionItem) => string,
196
+ recency: (item: CompletionItem) => number = () => 0,
197
+ limit = 150
198
+ ): Ranked[] {
199
+ return items
200
+ .map((item, index) => {
201
+ const prefix = prefixOf(item);
202
+ return { item, index, prefix, score: matchScore(item.filterText ?? item.label, prefix), recent: recency(item) };
203
+ })
204
+ .filter((entry): entry is typeof entry & { score: number } => entry.score !== undefined)
205
+ .sort(
206
+ (left, right) =>
207
+ left.score - right.score ||
208
+ right.recent - left.recent ||
209
+ Number(right.item.preselect === true) - Number(left.item.preselect === true) ||
210
+ (left.item.sortText ?? left.item.label).localeCompare(right.item.sortText ?? right.item.label) ||
211
+ left.index - right.index
212
+ )
213
+ .slice(0, limit)
214
+ .map(({ item, score, prefix }) => ({ item, score, prefix }));
215
+ }
216
+
217
+ /* The characters of a label to mark for what was typed; none when the label is not what the server filtered on. */
218
+ export function matchedCharacters(label: string, prefix: string): readonly number[] {
219
+ return matchDetail(label, prefix)?.positions ?? [];
220
+ }
221
+
222
+ /* The first line of what a server says about where an item comes from, with a PHP `use` made into the namespace it names. */
223
+ function sourceOf(item: CompletionItem): string {
224
+ const detail = item.detail?.split('\n')[0]?.trim() ?? '';
225
+ const imported = /^use\s+(.+)$/.exec(detail);
226
+ return imported === null ? detail : imported[1]!.slice(0, Math.max(0, imported[1]!.lastIndexOf('\\')));
227
+ }
228
+
229
+ /* What stands right-aligned after a row: the module or namespace the item comes from, else what the server calls its type. */
230
+ export function qualifierOf(item: CompletionItem): string {
231
+ const description = item.labelDetails?.description?.trim() ?? '';
232
+ return description !== '' ? description : sourceOf(item);
233
+ }
234
+
235
+ /*
236
+ * The qualifier of each row of a list. Items that share a label are told apart however it takes: by the
237
+ * qualifier, or by what the server says besides, and by a number when a server says nothing that differs.
238
+ */
239
+ export function qualifiersOf(items: readonly CompletionItem[]): string[] {
240
+ const result = items.map(qualifierOf);
241
+ const groups = new Map<string, number[]>();
242
+ items.forEach((item, index) => groups.set(item.label, [...(groups.get(item.label) ?? []), index]));
243
+ for (const indexes of groups.values()) {
244
+ if (indexes.length < 2 || new Set(indexes.map((index) => result[index])).size === indexes.length) {
245
+ continue;
246
+ }
247
+ const alternate = indexes.map((index) => sourceOf(items[index]!) || result[index]!);
248
+ const distinct = new Set(alternate).size === indexes.length && !alternate.includes('');
249
+ indexes.forEach((index, position) => {
250
+ result[index] = distinct ? alternate[position]! : `${result[index] ?? ''} #${position + 1}`.trim();
251
+ });
252
+ }
253
+ return result;
254
+ }
255
+
256
+ /* A snippet as the plain text it would insert: tab stops and variables vanish, placeholders and choices become their text, and the escapes of the format are undone. */
257
+ export function snippetToText(snippet: string): string {
258
+ return parseSnippet(snippet).text;
259
+ }
260
+
261
+ export const METHOD_KIND = 2;
262
+ export const FUNCTION_KIND = 3;
263
+ export const CONSTRUCTOR_KIND = 4;
264
+ export const CLASS_KIND = 7;
265
+ export const SNIPPET_FORMAT = 2;
266
+
267
+ /* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
268
+ function withoutCall(snippet: string): string {
269
+ const open = snippet.search(/(?<!\\)\(/);
270
+ return open > 0 ? snippet.slice(0, open) : snippet;
271
+ }
272
+
273
+ export interface Insertion {
274
+ readonly range: EditorRange;
275
+ readonly text: string;
276
+ /* Where Tab goes in the inserted text, in order; empty for plain text and for a snippet without stops. */
277
+ readonly stops: readonly SnippetStop[];
278
+ }
279
+
280
+ /*
281
+ * What goes into the text for an item and the range it takes the place of, given the caret. `lineAfter` is
282
+ * the rest of the line after the caret: an item that would add a call does not when a parenthesis follows.
283
+ */
284
+ export function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter = ''): Insertion {
285
+ const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
286
+ const edit = editRangeOf(item, replace);
287
+ const range =
288
+ edit === null
289
+ ? { start: { line: caret.line, character: caret.character - identifierPrefix(lineBefore).length }, end: caret }
290
+ : // The word grew since the answer came, so what was typed since belongs to what is replaced.
291
+ { start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
292
+ if (item.insertTextFormat !== SNIPPET_FORMAT) {
293
+ return { range, text: raw, stops: [] };
294
+ }
295
+ const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
296
+ const callable = item.kind === METHOD_KIND || item.kind === FUNCTION_KIND || item.kind === CONSTRUCTOR_KIND;
297
+ const parsed = parseSnippet(followed && callable ? withoutCall(raw) : raw);
298
+ return { range, text: parsed.text, stops: tabOrder(parsed) };
299
+ }
300
+
301
+ /*
302
+ * The same insertion at every other caret that has the word the primary caret has before it, so a name typed at
303
+ * several carets is completed at all of them. A caret that is inside a selection, or after other text, is left alone.
304
+ */
305
+ export function mirroredInsertions(
306
+ carets: readonly EditorRange[],
307
+ primary: EditorRange,
308
+ caret: EditorPosition,
309
+ text: (range: EditorRange) => string,
310
+ insertion: Insertion
311
+ ): EditorContentChange[] {
312
+ const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
313
+ const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
314
+ if (before < 0 || after < 0) {
315
+ return [];
316
+ }
317
+ const typed = text({ start: insertion.range.start, end: caret });
318
+ const replaced = text({ start: caret, end: insertion.range.end });
319
+ return carets.flatMap((other): EditorContentChange[] => {
320
+ const at = other.start;
321
+ if (comparePositions(other.start, primary.start) === 0 && comparePositions(other.end, primary.end) === 0) {
322
+ return [];
323
+ }
324
+ if (comparePositions(other.start, other.end) !== 0 || at.character < before) {
325
+ return [];
326
+ }
327
+ const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };
328
+ const preceding = range.start.character > 0 ? text({ start: { line: at.line, character: range.start.character - 1 }, end: range.start }) : '';
329
+ const startsWord = preceding === '' || !isIdentifierCharacter(preceding);
330
+ const matches = startsWord && text({ start: range.start, end: at }) === typed && text({ start: at, end: range.end }) === replaced;
331
+ return matches ? [{ range, text: insertion.text }] : [];
332
+ });
333
+ }
334
+
335
+ export function documentationText(documentation: string | MarkupContent | undefined): string {
336
+ if (documentation === undefined) {
337
+ return '';
338
+ }
339
+ return typeof documentation === 'string'
340
+ ? documentation
341
+ : documentation.kind === 'plaintext'
342
+ ? documentation.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1')
343
+ : documentation.value;
344
+ }
345
+
346
+ const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
347
+
348
+ /*
349
+ * What is said about a suggestion beside the list, in the shape a hover has: the signature it names and the
350
+ * documentation split into signature, prose and tags. A first line that only says where the item comes from is
351
+ * kept apart, since it is no code.
352
+ */
353
+ export function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null {
354
+ const lines = (item.detail ?? '').trim().split('\n');
355
+ const hasSource = SOURCE_LINE.test(lines[0] ?? '');
356
+ const source = hasSource ? lines[0]!.trim() : '';
357
+ const code = (hasSource ? lines.slice(1) : lines).join('\n').trim();
358
+ const parsed = splitSignatures(documentationText(item.documentation));
359
+ const text: HoverText = {
360
+ signatures: code === '' ? parsed.signatures : [{ language: highlightLanguage, code }, ...parsed.signatures],
361
+ markdown: parsed.markdown
362
+ };
363
+ return source === '' && isEmptyHover(text) ? null : { source, text };
364
+ }
365
+
366
+ export type { InsertReplaceEdit, TextEdit };
367
+
368
+ const KIND_LETTERS: Record<number, string> = {
369
+ 1: 't',
370
+ 2: 'm',
371
+ 3: 'f',
372
+ 4: 'c',
373
+ 5: 'p',
374
+ 6: 'v',
375
+ 7: 'c',
376
+ 8: 'i',
377
+ 9: 'm',
378
+ 10: 'p',
379
+ 11: 'u',
380
+ 12: 'v',
381
+ 13: 'e',
382
+ 14: 'k',
383
+ 15: 's',
384
+ 16: 'c',
385
+ 17: 'f',
386
+ 18: 'r',
387
+ 19: 'f',
388
+ 20: 'e',
389
+ 21: 'k',
390
+ 22: 's',
391
+ 23: 'e',
392
+ 24: 'o',
393
+ 25: 't'
394
+ };
395
+
396
+ export type KindTone = 'callable' | 'value' | 'type' | 'other';
397
+
398
+ const KIND_TONES: Record<number, KindTone> = {
399
+ 2: 'callable',
400
+ 3: 'callable',
401
+ 4: 'callable',
402
+ 5: 'value',
403
+ 6: 'value',
404
+ 10: 'value',
405
+ 12: 'value',
406
+ 20: 'value',
407
+ 21: 'value',
408
+ 7: 'type',
409
+ 8: 'type',
410
+ 9: 'type',
411
+ 13: 'type',
412
+ 22: 'type',
413
+ 25: 'type'
414
+ };
415
+
416
+ export function kindLetterOf(kind: number | undefined): string {
417
+ return kind === undefined ? '·' : (KIND_LETTERS[kind] ?? '·');
418
+ }
419
+
420
+ export function kindToneOf(kind: number | undefined): KindTone {
421
+ return kind === undefined ? 'other' : (KIND_TONES[kind] ?? 'other');
422
+ }
423
+
424
+ export interface RowWindow {
425
+ readonly first: number;
426
+ /* One past the last row drawn. */
427
+ readonly last: number;
428
+ }
429
+
430
+ /*
431
+ * The rows of a list worth drawing: the ones in view, a few on each side, and the active one with the
432
+ * same margin, so the rows between never leave a gap. The rest is only space.
433
+ */
434
+ export function rowWindow(scrollTop: number, active: number, total: number, rowHeight: number, visible: number, margin: number, padding: number): RowWindow {
435
+ const shown = Math.floor(Math.max(0, scrollTop - padding) / rowHeight);
436
+ return {
437
+ first: Math.max(0, Math.min(shown, active) - margin),
438
+ last: Math.min(total, Math.max(shown + visible + 1, active + 1) + margin)
439
+ };
440
+ }