@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,230 @@
1
+ import type { DocumentSymbol, DocumentSymbolResult, Position, SymbolInformation } from '@adecore/lsp';
2
+ import { formatNumber } from '@adecore/ui/format';
3
+ import type { i18n } from 'i18next';
4
+
5
+ /* More declarations than this in one file and the rows are left out, which is the file that is too big for them to be quiet. */
6
+ export const MAX_DECLARATIONS = 3000;
7
+
8
+ /* LSP's SymbolKind: the kinds a row is drawn for and the ones whose children can be declarations too. */
9
+ const CLASS = 5;
10
+ const METHOD = 6;
11
+ const PROPERTY = 7;
12
+ const FIELD = 8;
13
+ const CONSTRUCTOR = 9;
14
+ const ENUM = 10;
15
+ const INTERFACE = 11;
16
+ const FUNCTION = 12;
17
+ const CONSTANT = 14;
18
+ const STRUCT = 23;
19
+ const CONTAINERS: ReadonlySet<number> = new Set([1, 2, 3, 4, CLASS, ENUM, INTERFACE, STRUCT]);
20
+ const TYPES: ReadonlySet<number> = new Set([CLASS, ENUM, INTERFACE, STRUCT]);
21
+ const CALLABLES: ReadonlySet<number> = new Set([METHOD, FUNCTION]);
22
+
23
+ export interface CodeVisionDeclaration {
24
+ /* The same declaration keeps its id through edits, so what a row showed stays until the new answer is in. */
25
+ readonly id: string;
26
+ readonly name: string;
27
+ /* Where the name starts, which is where the references are asked. */
28
+ readonly position: Position;
29
+ /* The line the row sits above: where the declaration starts once the comments before it are passed. */
30
+ readonly line: number;
31
+ readonly usages: boolean;
32
+ readonly authors: boolean;
33
+ /* The lines whose authors count: from the name to the last line that holds more than closing brackets. */
34
+ readonly authorFrom: number;
35
+ readonly authorTo: number;
36
+ }
37
+
38
+ export interface DeclarationSource {
39
+ readonly lineCount: number;
40
+ lineAt(line: number): string;
41
+ readonly languageId: string;
42
+ }
43
+
44
+ /* Languages whose `#` starts a comment, so a docblock written with it is passed like any other. */
45
+ const HASH_COMMENTS: ReadonlySet<string> = new Set(['php', 'python', 'shellscript', 'ruby', 'yaml', 'dockerfile']);
46
+
47
+ interface Candidate {
48
+ readonly name: string;
49
+ readonly kind: number;
50
+ readonly start: Position;
51
+ readonly end: Position;
52
+ readonly namePosition: Position | null;
53
+ readonly path: readonly string[];
54
+ readonly parentKind: number | null;
55
+ }
56
+
57
+ function isLocalName(name: string): boolean {
58
+ return name === '' || name.startsWith('<') || name === 'default';
59
+ }
60
+
61
+ function isUsageKind(kind: number, parentKind: number | null): boolean {
62
+ if (TYPES.has(kind) || CALLABLES.has(kind)) {
63
+ return true;
64
+ }
65
+ return (kind === PROPERTY || kind === FIELD || kind === CONSTANT) && (parentKind === CLASS || parentKind === STRUCT);
66
+ }
67
+
68
+ function isAuthorKind(kind: number): boolean {
69
+ return TYPES.has(kind) || CALLABLES.has(kind) || kind === CONSTRUCTOR;
70
+ }
71
+
72
+ function walk(symbols: readonly DocumentSymbol[], path: readonly string[], parentKind: number | null, out: Candidate[]): void {
73
+ for (const symbol of symbols) {
74
+ out.push({
75
+ name: symbol.name,
76
+ kind: symbol.kind,
77
+ start: symbol.range.start,
78
+ end: symbol.range.end,
79
+ namePosition: symbol.selectionRange.start,
80
+ path,
81
+ parentKind
82
+ });
83
+ // What a function or a method holds is local to it, and a row for a callback is noise.
84
+ if (CONTAINERS.has(symbol.kind)) {
85
+ walk(symbol.children ?? [], [...path, symbol.name], symbol.kind, out);
86
+ }
87
+ }
88
+ }
89
+
90
+ function candidatesOf(result: DocumentSymbolResult): Candidate[] {
91
+ const out: Candidate[] = [];
92
+ if (result === null || result.length === 0) {
93
+ return out;
94
+ }
95
+ if ('range' in result[0]!) {
96
+ walk(result as DocumentSymbol[], [], null, out);
97
+ return out;
98
+ }
99
+ const flat = result as SymbolInformation[];
100
+ const containers = new Map(flat.filter((symbol) => CONTAINERS.has(symbol.kind)).map((symbol) => [symbol.name, symbol.kind]));
101
+ for (const symbol of flat) {
102
+ const container = symbol.containerName ?? '';
103
+ if (container !== '' && !containers.has(container)) {
104
+ continue;
105
+ }
106
+ out.push({
107
+ name: symbol.name,
108
+ kind: symbol.kind,
109
+ start: symbol.location.range.start,
110
+ end: symbol.location.range.end,
111
+ namePosition: null,
112
+ path: container === '' ? [] : [container],
113
+ parentKind: container === '' ? null : (containers.get(container) ?? null)
114
+ });
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /*
120
+ * Where a declaration really starts: past the comments and the blank lines a server counts into its
121
+ * range. Null when its range is nothing but comments.
122
+ */
123
+ export function startAfterComments(source: DeclarationSource, from: Position, to: Position): Position | null {
124
+ const hash = HASH_COMMENTS.has(source.languageId);
125
+ let line = from.line;
126
+ let character = from.character;
127
+ while (line <= to.line && line < source.lineCount) {
128
+ const text = source.lineAt(line);
129
+ let at = character;
130
+ while (at < text.length && (text[at] === ' ' || text[at] === '\t')) {
131
+ at++;
132
+ }
133
+ if (at >= text.length) {
134
+ line++;
135
+ character = 0;
136
+ continue;
137
+ }
138
+ if (text.startsWith('/*', at)) {
139
+ let end = text.indexOf('*/', at + 2);
140
+ let scan = line;
141
+ while (end === -1 && scan + 1 <= to.line && scan + 1 < source.lineCount) {
142
+ scan++;
143
+ end = source.lineAt(scan).indexOf('*/');
144
+ }
145
+ if (end === -1) {
146
+ return null;
147
+ }
148
+ line = scan;
149
+ character = end + 2;
150
+ continue;
151
+ }
152
+ if (text.startsWith('//', at) || (hash && text[at] === '#' && text[at + 1] !== '[')) {
153
+ line++;
154
+ character = 0;
155
+ continue;
156
+ }
157
+ return line > to.line ? null : { line, character: at };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ const CLOSERS = /^\s*[})\]]*[;,]?\s*$/;
163
+
164
+ /* The last line of a declaration that holds more than the brackets that close it or nothing, which is what its authors are read from. */
165
+ function lastLineWithCode(source: DeclarationSource, from: number, to: number): number {
166
+ for (let line = Math.min(to, source.lineCount - 1); line > from; line--) {
167
+ if (!CLOSERS.test(source.lineAt(line))) {
168
+ return line;
169
+ }
170
+ }
171
+ return from;
172
+ }
173
+
174
+ /* Where the name starts when a server gave only a range: its first occurrence on the line the declaration starts on. */
175
+ function nameIn(source: DeclarationSource, name: string, start: Position): Position | null {
176
+ const at = source.lineAt(start.line).indexOf(name, start.character);
177
+ return at === -1 ? null : { line: start.line, character: at };
178
+ }
179
+
180
+ /*
181
+ * The declarations of a file a row is drawn above: classes, interfaces, enums, structs, functions and
182
+ * methods, and the properties and constants of a class, wherever they stand outside the body of a
183
+ * function. Only one that starts its line gets a row, since a row sits above the line.
184
+ */
185
+ export function declarationsOf(result: DocumentSymbolResult, source: DeclarationSource): CodeVisionDeclaration[] {
186
+ const declarations: CodeVisionDeclaration[] = [];
187
+ const seen = new Map<string, number>();
188
+ for (const candidate of candidatesOf(result)) {
189
+ if (isLocalName(candidate.name)) {
190
+ continue;
191
+ }
192
+ const usages = isUsageKind(candidate.kind, candidate.parentKind) && !candidate.name.startsWith('__');
193
+ const authors = isAuthorKind(candidate.kind);
194
+ if (!usages && !authors) {
195
+ continue;
196
+ }
197
+ const start = startAfterComments(source, candidate.start, candidate.end);
198
+ if (start === null || source.lineAt(start.line).slice(0, start.character).trim() !== '') {
199
+ continue;
200
+ }
201
+ const position = candidate.namePosition ?? nameIn(source, candidate.name, start);
202
+ if (position === null) {
203
+ continue;
204
+ }
205
+ const key = `${candidate.path.join('>')}>${candidate.name}#${candidate.kind}`;
206
+ const index = seen.get(key) ?? 0;
207
+ seen.set(key, index + 1);
208
+ declarations.push({
209
+ id: `${key}#${index}`,
210
+ name: candidate.name,
211
+ position,
212
+ line: start.line,
213
+ usages,
214
+ authors,
215
+ authorFrom: position.line,
216
+ authorTo: lastLineWithCode(source, position.line, candidate.end.line)
217
+ });
218
+ if (declarations.length > MAX_DECLARATIONS) {
219
+ return [];
220
+ }
221
+ }
222
+ return declarations;
223
+ }
224
+
225
+ /* "No usages", "1 usage" and "N usages". */
226
+ export function usagesText(count: number, i18n: i18n): string {
227
+ return count === 0
228
+ ? i18n.t('editor:language.codeVision.usagesNone')
229
+ : i18n.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
230
+ }
@@ -0,0 +1,269 @@
1
+ import { splitLines } from '@adecore/merge';
2
+ import type { GitBlameResult } from './host-types.ts';
3
+ import type { EditorCodeVision, EditorCodeVisionEntry, EditorRect } from '@adecore/editor';
4
+ import type { DocumentSymbolResult } from '@adecore/lsp';
5
+ import type { EditorLanguage } from './editor-language.ts';
6
+ import { type CodeAuthorship, authorsText, authorshipOf, mapBlame } from './code-authors.ts';
7
+ import { type CodeVisionDeclaration, declarationsOf, usagesText } from './code-vision-model.ts';
8
+ import { realTimers, type Timers } from './timers.ts';
9
+
10
+ const REFERENCES = 'textDocument/references';
11
+ /* Lines asked about above and below what is in view, so a little scrolling stays inside what is counted. */
12
+ const MARGIN_LINES = 40;
13
+ /* References are the slowest question a server is asked here, so only this many are in flight at once. */
14
+ const MAX_PARALLEL = 3;
15
+ const SCROLL_PAUSE_MS = 150;
16
+ /* Past this many lines a file gets no rows at all, which is also what keeps blame and the walk of symbols cheap. */
17
+ export const MAX_LINES = 20_000;
18
+
19
+ export interface CodeVisionSettings {
20
+ readonly usages: boolean;
21
+ readonly authors: boolean;
22
+ }
23
+
24
+ /*
25
+ * What git says about the file: `pending` while it is being asked, so the rows are held for the authors
26
+ * that will come, and the blame of the text on disk once it is in. Null where git has nothing to say.
27
+ */
28
+ export type BlameSource =
29
+ | { readonly kind: 'pending' }
30
+ | { readonly kind: 'ready'; readonly blame: GitBlameResult; readonly base: string; openCommit?(hash: string): void };
31
+
32
+ /*
33
+ * The rows above declarations: how often each is used and, once git has been asked, who wrote it. The
34
+ * declarations come from the symbols the servers know, and a row holds its height from the first
35
+ * answer on, empty until its entries are in, so the text never moves when they arrive. Counts are asked
36
+ * lazily, for the declarations in view and a margin around them, a few at a time, and only after the
37
+ * text has settled; an old count stays on its row until the new one is in.
38
+ */
39
+ export class CodeVisionFeature {
40
+ private readonly language: EditorLanguage;
41
+ private readonly timers: Timers;
42
+ private settings: CodeVisionSettings = { usages: false, authors: false };
43
+ private declarations: CodeVisionDeclaration[] = [];
44
+ /* What the declarations were read from, so a count never outlives the text it was asked for. */
45
+ private epoch = 0;
46
+ private edited = false;
47
+ private readonly counts = new Map<string, { count: number; epoch: number }>();
48
+ private queue: CodeVisionDeclaration[] = [];
49
+ /* Scrolling changes the pending queue but keeps requests already in flight. */
50
+ private readonly running = new Map<string, AbortController>();
51
+ /* Declarations a server could not count for this text, which are not asked again until it says it can. */
52
+ private failed = new Set<string>();
53
+ private blame: BlameSource | null = null;
54
+ /* The text the declarations were read from, which the lines of their authors are counted in. */
55
+ private lines: readonly string[] = [];
56
+ private mapped: { blame: GitBlameResult; lines: Int32Array | null } | null = null;
57
+ private readonly authorships = new Map<string, CodeAuthorship>();
58
+ private scrollTimer: unknown;
59
+ private publishing = false;
60
+ private disposed = false;
61
+
62
+ constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
63
+ this.language = language;
64
+ this.timers = timers;
65
+ const { editor } = language;
66
+ const offs = [
67
+ onSymbols((result) => this.read(result)),
68
+ editor.onTextChange(() => {
69
+ this.edited = true;
70
+ this.closeAuthors();
71
+ }),
72
+ editor.onViewChange(() => {
73
+ this.timers.clear(this.scrollTimer);
74
+ this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
75
+ })
76
+ ];
77
+ const providers = language.onProvidersChanged(() => {
78
+ this.failed.clear();
79
+ this.publish();
80
+ this.plan();
81
+ });
82
+ language.onDispose(() => {
83
+ this.disposed = true;
84
+ for (const off of offs) {
85
+ off();
86
+ }
87
+ providers();
88
+ this.timers.clear(this.scrollTimer);
89
+ for (const controller of this.running.values()) {
90
+ controller.abort();
91
+ }
92
+ this.closeAuthors();
93
+ editor.setCodeVision([]);
94
+ });
95
+ }
96
+
97
+ /* Which rows to draw, from the settings of the client. */
98
+ configure(settings: CodeVisionSettings): void {
99
+ if (settings.usages === this.settings.usages && settings.authors === this.settings.authors) {
100
+ return;
101
+ }
102
+ this.settings = settings;
103
+ this.publish();
104
+ this.plan();
105
+ }
106
+
107
+ /* What git says about the file, from the client that asked it. */
108
+ setBlame(blame: BlameSource | null): void {
109
+ this.blame = blame;
110
+ this.mapped = null;
111
+ this.authorships.clear();
112
+ this.publish();
113
+ }
114
+
115
+ private read(result: DocumentSymbolResult): void {
116
+ const { editor, languageId } = this.language;
117
+ const lines = splitLines(editor.getText());
118
+ this.lines = lines;
119
+ this.mapped = null;
120
+ this.authorships.clear();
121
+ this.edited = false;
122
+ this.declarations =
123
+ lines.length > MAX_LINES ? [] : declarationsOf(result, { lineCount: lines.length, lineAt: (line) => lines[line] ?? '', languageId });
124
+ this.epoch++;
125
+ this.failed.clear();
126
+ this.publish();
127
+ this.plan();
128
+ }
129
+
130
+ private get canCount(): boolean {
131
+ return this.settings.usages && this.language.project.service.supports(REFERENCES, this.language.uri);
132
+ }
133
+
134
+ /* Queues the declarations in view whose count is not from the current text, nearest the top of the view first. */
135
+ private plan(): void {
136
+ if (this.disposed || this.edited || !this.canCount) {
137
+ return;
138
+ }
139
+ const view = this.language.editor.getVisibleRange();
140
+ const first = view.start.line - MARGIN_LINES;
141
+ const last = view.end.line + MARGIN_LINES;
142
+ this.queue = this.declarations
143
+ .filter(
144
+ (declaration) =>
145
+ declaration.usages &&
146
+ declaration.line >= first &&
147
+ declaration.line <= last &&
148
+ this.counts.get(declaration.id)?.epoch !== this.epoch &&
149
+ !this.running.has(declaration.id) &&
150
+ !this.failed.has(declaration.id)
151
+ )
152
+ .sort((left, right) => Math.abs(left.line - view.start.line) - Math.abs(right.line - view.start.line));
153
+ while (this.running.size < MAX_PARALLEL && this.queue.length > 0) {
154
+ void this.count(this.queue.shift()!);
155
+ }
156
+ }
157
+
158
+ private async count(declaration: CodeVisionDeclaration): Promise<void> {
159
+ const { project, uri } = this.language;
160
+ const controller = new AbortController();
161
+ this.running.set(declaration.id, controller);
162
+ const epoch = this.epoch;
163
+ try {
164
+ const locations = await project.service.references(uri, declaration.position, false, { signal: controller.signal, parallel: true });
165
+ if (!this.disposed && epoch === this.epoch) {
166
+ this.counts.set(declaration.id, { count: distinctPlaces(locations ?? []), epoch });
167
+ this.publish();
168
+ }
169
+ } catch {
170
+ // A text that moved on is counted again from its own symbols; for a server that is not up the declaration waits for it to say so.
171
+ if (epoch === this.epoch) {
172
+ this.failed.add(declaration.id);
173
+ }
174
+ } finally {
175
+ this.running.delete(declaration.id);
176
+ this.plan();
177
+ }
178
+ }
179
+
180
+ /* Said once for every change that lands in the same turn, since each is a pass over the rows of the layout. */
181
+ private publish(): void {
182
+ if (this.publishing) {
183
+ return;
184
+ }
185
+ this.publishing = true;
186
+ queueMicrotask(() => {
187
+ this.publishing = false;
188
+ if (!this.disposed) {
189
+ this.language.editor.setCodeVision(this.rows());
190
+ }
191
+ });
192
+ }
193
+
194
+ private rows(): EditorCodeVision[] {
195
+ const rows: EditorCodeVision[] = [];
196
+ const taken = new Set<number>();
197
+ const counted = this.canCount;
198
+ const withAuthors = this.settings.authors && this.blame !== null;
199
+ const commits = this.blame?.kind === 'ready' ? this.blame.blame.commits : [];
200
+ const mapped = this.blame?.kind === 'ready' && this.settings.authors ? this.mapOfBlame(this.blame) : null;
201
+ for (const declaration of this.declarations) {
202
+ const reserved = (counted && declaration.usages) || (withAuthors && declaration.authors);
203
+ if (!reserved || taken.has(declaration.line)) {
204
+ continue;
205
+ }
206
+ taken.add(declaration.line);
207
+ const entries: EditorCodeVisionEntry[] = [];
208
+ const known = this.counts.get(declaration.id);
209
+ if (counted && declaration.usages && known !== undefined) {
210
+ entries.push({
211
+ id: 'usages',
212
+ text: usagesText(known.count, this.language.project.i18n),
213
+ activate: () => void this.language.peek.open(declaration.position)
214
+ });
215
+ }
216
+ if (mapped !== null && declaration.authors) {
217
+ let authorship = this.authorships.get(declaration.id);
218
+ if (authorship === undefined) {
219
+ authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
220
+ this.authorships.set(declaration.id, authorship);
221
+ }
222
+ const shown = authorship;
223
+ entries.push({
224
+ id: 'authors',
225
+ text: authorsText(shown, this.language.project.i18n),
226
+ ...(shown.authors.length > 0 ? { icon: shown.authors.length > 1 ? ('users' as const) : ('user' as const) } : {}),
227
+ activate: (anchor) => this.showAuthors(declaration, shown, anchor)
228
+ });
229
+ }
230
+ rows.push({ id: declaration.id, line: declaration.line, entries });
231
+ }
232
+ return rows;
233
+ }
234
+
235
+ private mapOfBlame(source: Extract<BlameSource, { kind: 'ready' }>): Int32Array | null {
236
+ if (this.mapped?.blame !== source.blame) {
237
+ this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
238
+ }
239
+ return this.mapped.lines;
240
+ }
241
+
242
+ private showAuthors(declaration: CodeVisionDeclaration, authorship: CodeAuthorship, anchor: EditorRect): void {
243
+ const source = this.blame;
244
+ const openCommit = source?.kind === 'ready' ? source.openCommit : undefined;
245
+ this.language.popups.setState({
246
+ authors: {
247
+ anchor,
248
+ name: declaration.name,
249
+ authorship,
250
+ openCommit: openCommit === undefined || authorship.latest === null ? null : () => openCommit(authorship.latest!.hash)
251
+ }
252
+ });
253
+ }
254
+
255
+ /* Closes the card of the authors; `restoreFocus` is for a close that came from the keyboard, which gives the editor its keys back. */
256
+ closeAuthors(restoreFocus = false): void {
257
+ if (this.language.popups.getState().authors !== null) {
258
+ this.language.popups.setState({ authors: null });
259
+ if (restoreFocus) {
260
+ this.language.editor.focus();
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ /* The places a server named, each counted once however many ways it was reached. */
267
+ function distinctPlaces(locations: readonly { uri: string; range: { start: { line: number; character: number } } }[]): number {
268
+ return new Set(locations.map((location) => `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`)).size;
269
+ }
@@ -0,0 +1,139 @@
1
+ import type { CompletionItem } from '@adecore/lsp';
2
+ import { CLASS_KIND, CONSTRUCTOR_KIND, FUNCTION_KIND, METHOD_KIND, SNIPPET_FORMAT, type Insertion } from './completion-model.ts';
3
+
4
+ /* What the platform's editors send to ask for parameter info once an item is in; the card is ours to open. */
5
+ export const PARAMETER_HINTS_COMMAND = 'editor.action.triggerParameterHints';
6
+
7
+ const CALL_LANGUAGES = new Set(['typescript', 'typescriptreact', 'javascript', 'javascriptreact', 'vue', 'php', 'python']);
8
+ const MARKUP_LANGUAGES = new Set(['typescriptreact', 'javascriptreact', 'vue']);
9
+
10
+ const IMPORT_BRACE = /\b(?:import|export)(?:\s+type)?(?:\s+[\w$]+\s*,)?\s*$/;
11
+ const PHP_USE_BRACE = /\buse(?:\s+(?:function|const))?\s+[\w\\]*\\\s*$/;
12
+ const TYPE_OPERATOR = /\b(?:typeof|keyof|instanceof|implements|satisfies|as)\s+[\w$.?]*$/;
13
+ const AFTER_NEW = /\bnew\s+[\w$.\\]*$/;
14
+ const EXISTING_CALL = /^\s*\(/;
15
+ const SIGNATURE_KIND_PREFIX = /^\([a-z]+(?: [a-z]+)*\)\s+(?!=>)/;
16
+ const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
17
+
18
+ /*
19
+ * What accepting an item does about the call it names: nothing, parentheses added with the caret inside or
20
+ * after them, or the parenthesis that is already there, which the caret goes into on Tab and stays out of on Enter.
21
+ */
22
+ export type CallPlan =
23
+ | { readonly action: 'none' }
24
+ | { readonly action: 'add'; readonly inside: boolean }
25
+ | { readonly action: 'enter'; readonly shift: number };
26
+
27
+ export interface CallSite {
28
+ readonly item: CompletionItem;
29
+ readonly languageId: string;
30
+ readonly insertion: Insertion;
31
+ /* The text before the insertion, from some lines up, so a list that spans lines is still seen whole. */
32
+ readonly before: string;
33
+ /* The rest of the line after what the insertion replaces. */
34
+ readonly rest: string;
35
+ readonly replace: boolean;
36
+ }
37
+
38
+ /* Whether the item is something that is called where it stands: a function, a method, a constructor, or a class after `new`. */
39
+ export function isCallItem(item: CompletionItem, lineBefore: string): boolean {
40
+ if (item.kind === METHOD_KIND || item.kind === FUNCTION_KIND || item.kind === CONSTRUCTOR_KIND) {
41
+ return true;
42
+ }
43
+ return item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore);
44
+ }
45
+
46
+ /* Whether the parameter list in a signature has anything in it; null when the text names no list. */
47
+ function listedParameters(signature: string): boolean | null {
48
+ const text = signature.replace(SIGNATURE_KIND_PREFIX, '');
49
+ const open = text.indexOf('(');
50
+ if (open < 0) {
51
+ return null;
52
+ }
53
+ let depth = 0;
54
+ for (let i = open; i < text.length; i++) {
55
+ if (text[i] === '(') {
56
+ depth++;
57
+ } else if (text[i] === ')' && --depth === 0) {
58
+ return text.slice(open + 1, i).trim() !== '';
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /* From what the server says of the item: `(method) Box.open(flag: boolean): void` has parameters and `(): void` has none. Null when it says nothing about them. */
65
+ export function hasParameters(item: CompletionItem): boolean | null {
66
+ const lines = (item.detail ?? '').split('\n').filter((line) => !SOURCE_LINE.test(line.trim()));
67
+ for (const signature of [item.labelDetails?.detail ?? '', ...lines]) {
68
+ const listed = listedParameters(signature);
69
+ if (listed !== null) {
70
+ return listed;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /* The text before the innermost brace that is still open, which says whether the caret is in an import or export list. */
77
+ function beforeOpenBrace(before: string): string | null {
78
+ let depth = 0;
79
+ for (let i = before.length - 1; i >= 0; i--) {
80
+ if (before[i] === '}') {
81
+ depth++;
82
+ } else if (before[i] === '{' && depth-- === 0) {
83
+ return before.slice(0, i);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /* Where a name is not called: in an import or export list, a `use`, a type operator's operand, an attribute or a tag. */
90
+ function isNameOnly(languageId: string, before: string): boolean {
91
+ const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
92
+ const brace = beforeOpenBrace(before);
93
+ if (brace !== null && (IMPORT_BRACE.test(brace) || (languageId === 'php' && PHP_USE_BRACE.test(brace)))) {
94
+ return true;
95
+ }
96
+ if (TYPE_OPERATOR.test(lineBefore) || /^\s*export\s+default\s+[\w$]*$/.test(lineBefore)) {
97
+ return true;
98
+ }
99
+ switch (languageId) {
100
+ case 'php':
101
+ return /^\s*use\s/.test(lineBefore) || /#\[[^\]]*$/.test(lineBefore);
102
+ case 'python':
103
+ return /^\s*(?:from\s+\S+\s+)?import\b/.test(lineBefore);
104
+ default:
105
+ return /^\s*import\b/.test(lineBefore) || (MARKUP_LANGUAGES.has(languageId) && /<\/?[\w$.:-]*$/.test(lineBefore));
106
+ }
107
+ }
108
+
109
+ /*
110
+ * Decides what accepting `item` does about a call, as the platform's parentheses handler does: a callable
111
+ * gets `()` with the caret inside when it has parameters (or when that is not known) and behind when it has
112
+ * none, and one that already has a parenthesis after it gets none. A snippet, or text that already holds a
113
+ * parenthesis, carries its own call and wins.
114
+ */
115
+ export function planCall(site: CallSite): CallPlan {
116
+ const { item, languageId, insertion, before, rest, replace } = site;
117
+ const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
118
+ if (!CALL_LANGUAGES.has(languageId) || !isCallItem(item, lineBefore) || item.insertTextFormat === SNIPPET_FORMAT || insertion.text.includes('(')) {
119
+ return { action: 'none' };
120
+ }
121
+ if (isNameOnly(languageId, before)) {
122
+ return { action: 'none' };
123
+ }
124
+ const existing = EXISTING_CALL.exec(rest);
125
+ if (existing !== null) {
126
+ return replace ? { action: 'enter', shift: existing[0].length } : { action: 'none' };
127
+ }
128
+ if (rest.startsWith('<')) {
129
+ return { action: 'none' };
130
+ }
131
+ return { action: 'add', inside: hasParameters(item) !== false };
132
+ }
133
+
134
+ /* The insertion with its parentheses, and the one tab stop that puts the caret where the plan says. */
135
+ export function withParentheses(insertion: Insertion, inside: boolean): Insertion {
136
+ const text = `${insertion.text}()`;
137
+ const caret = inside ? text.length - 1 : text.length;
138
+ return { range: insertion.range, text, stops: [{ index: 0, start: caret, end: caret }] };
139
+ }