@adecore/editor-react 0.22.0 → 0.23.0

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 (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
@@ -0,0 +1,388 @@
1
+ import { parseHunkHeader } from '@adecore/merge';
2
+
3
+ export type PatchLineRole = 'file-header' | 'hunk-header' | 'context' | 'added' | 'removed' | 'no-newline' | 'binary' | 'other';
4
+
5
+ export type PatchFileStatus = 'added' | 'deleted' | 'renamed' | 'copied' | 'modified' | 'binary';
6
+
7
+ /* What a line of a patch is, and the one-based numbers it has in the files; null where the file has no such line. */
8
+ export interface PatchLayoutLine {
9
+ readonly role: PatchLineRole;
10
+ readonly old: number | null;
11
+ readonly new: number | null;
12
+ }
13
+
14
+ export interface PatchHunkLayout {
15
+ /* Zero-based line of the `@@` header in the patch text. */
16
+ readonly line: number;
17
+ /* As the header says them, `oldStart` and `newStart` one-based. */
18
+ readonly oldStart: number;
19
+ readonly oldCount: number;
20
+ readonly newStart: number;
21
+ readonly newCount: number;
22
+ readonly section: string;
23
+ /* False when the lines under the header no longer add up to its counts, such as after an edit. */
24
+ readonly consistent: boolean;
25
+ }
26
+
27
+ export interface PatchFileLayout {
28
+ /* Null for `/dev/null`, or when the patch does not say. Quotes and prefixes such as `a/` are gone. */
29
+ readonly oldPath: string | null;
30
+ readonly newPath: string | null;
31
+ readonly status: PatchFileStatus;
32
+ /* Zero-based lines of the section in the patch text, `lastLine` included. */
33
+ readonly firstLine: number;
34
+ readonly lastLine: number;
35
+ readonly added: number;
36
+ readonly removed: number;
37
+ readonly hunks: readonly PatchHunkLayout[];
38
+ }
39
+
40
+ export interface PatchLayout {
41
+ /* One entry per line of the text, `split('\n')`, up to the limit. */
42
+ readonly lines: readonly PatchLayoutLine[];
43
+ readonly files: readonly PatchFileLayout[];
44
+ /* The text went past a limit: the lines after it have no role and no file. */
45
+ readonly truncated: boolean;
46
+ }
47
+
48
+ export interface PatchLimits {
49
+ /* Lines read before the layout stops; defaults to 50,000. */
50
+ readonly maxLines?: number;
51
+ /* Characters read before the layout stops; defaults to 4,000,000. */
52
+ readonly maxCharacters?: number;
53
+ }
54
+
55
+ export const DEFAULT_PATCH_LIMITS = { maxLines: 50_000, maxCharacters: 4_000_000 } as const;
56
+
57
+ const OTHER: PatchLayoutLine = { role: 'other', old: null, new: null };
58
+ const FILE_HEADER: PatchLayoutLine = { role: 'file-header', old: null, new: null };
59
+ const HUNK_HEADER: PatchLayoutLine = { role: 'hunk-header', old: null, new: null };
60
+ const NO_NEWLINE: PatchLayoutLine = { role: 'no-newline', old: null, new: null };
61
+ const BINARY: PatchLayoutLine = { role: 'binary', old: null, new: null };
62
+
63
+ const GIT_HEADERS =
64
+ /^(?:index |old mode |new mode |deleted file mode |new file mode |similarity index |dissimilarity index |rename from |rename to |copy from |copy to )/;
65
+
66
+ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
67
+
68
+ interface Section {
69
+ oldPath: string | null;
70
+ newPath: string | null;
71
+ git: boolean;
72
+ /* Whether the `diff --git` line showed `a/X b/Y`, which is the only time `a/` and `b/` are not part of the path. */
73
+ prefixed: boolean;
74
+ created: boolean;
75
+ deleted: boolean;
76
+ renamed: boolean;
77
+ copied: boolean;
78
+ binary: boolean;
79
+ /* Whether `---` and `+++` named the files, which then beat the `diff --git` line. */
80
+ named: boolean;
81
+ firstLine: number;
82
+ lastLine: number;
83
+ added: number;
84
+ removed: number;
85
+ hunks: Mutable<PatchHunkLayout>[];
86
+ }
87
+
88
+ /* The hunk lines are being read into, with what its header still promises. */
89
+ interface OpenHunk {
90
+ layout: Mutable<PatchHunkLayout>;
91
+ oldLeft: number;
92
+ newLeft: number;
93
+ oldNumber: number;
94
+ newNumber: number;
95
+ }
96
+
97
+ const ESCAPES: Readonly<Record<string, number>> = { a: 7, b: 8, f: 12, n: 10, r: 13, t: 9, v: 11, '"': 34, '\\': 92 };
98
+
99
+ /* Reads a path as git writes it: bare, or in quotes with C escapes and octal bytes. */
100
+ function unquote(raw: string): string {
101
+ if (raw.length < 2 || !raw.startsWith('"') || !raw.endsWith('"')) {
102
+ return raw;
103
+ }
104
+ const bytes: number[] = [];
105
+ const encoder = new TextEncoder();
106
+ const body = raw.slice(1, -1);
107
+ for (let i = 0; i < body.length; i++) {
108
+ if (body[i] !== '\\') {
109
+ const point = String.fromCodePoint(body.codePointAt(i)!);
110
+ bytes.push(...encoder.encode(point));
111
+ i += point.length - 1;
112
+ continue;
113
+ }
114
+ const octal = /^[0-3][0-7]{2}/.exec(body.slice(i + 1, i + 4));
115
+ const escaped = ESCAPES[body[i + 1] ?? ''];
116
+ if (octal !== null) {
117
+ bytes.push(Number.parseInt(octal[0], 8));
118
+ i += 3;
119
+ } else if (escaped !== undefined) {
120
+ bytes.push(escaped);
121
+ i++;
122
+ } else {
123
+ bytes.push(92);
124
+ }
125
+ }
126
+ return new TextDecoder().decode(new Uint8Array(bytes));
127
+ }
128
+
129
+ /* The name on a `---`, `+++`, `rename` or `copy` line: a timestamp after a tab goes, and `/dev/null` is no file. */
130
+ function pathOf(raw: string): string | null {
131
+ const tab = raw.indexOf('\t');
132
+ const name = unquote(tab === -1 ? raw : raw.slice(0, tab));
133
+ return name === '/dev/null' ? null : name;
134
+ }
135
+
136
+ function stripPrefix(path: string | null, prefix: 'a/' | 'b/'): string | null {
137
+ return path !== null && path.startsWith(prefix) ? path.slice(2) : path;
138
+ }
139
+
140
+ /* The two names of a `diff --git a/x b/y` line; with spaces in an unquoted name, the split that gives both halves the same name wins. */
141
+ function gitPaths(rest: string): { oldPath: string | null; newPath: string | null; prefixed: boolean } {
142
+ if (rest.startsWith('"')) {
143
+ const end = rest.indexOf('" ', 1);
144
+ if (end !== -1) {
145
+ const oldName = unquote(rest.slice(0, end + 1));
146
+ const newName = unquote(rest.slice(end + 2));
147
+ const prefixed = oldName.startsWith('a/') && newName.startsWith('b/');
148
+ return prefixed ? { oldPath: oldName.slice(2), newPath: newName.slice(2), prefixed } : { oldPath: oldName, newPath: newName, prefixed };
149
+ }
150
+ }
151
+ const middle = rest.indexOf(' b/');
152
+ if (rest.startsWith('a/') && middle !== -1) {
153
+ for (let at = middle; at !== -1; at = rest.indexOf(' b/', at + 1)) {
154
+ if (rest.slice(2, at) === rest.slice(at + 3)) {
155
+ return { oldPath: rest.slice(2, at), newPath: rest.slice(at + 3), prefixed: true };
156
+ }
157
+ }
158
+ return { oldPath: rest.slice(2, middle), newPath: unquote(rest.slice(middle + 3)), prefixed: true };
159
+ }
160
+ const split = rest.indexOf(' ');
161
+ return split === -1
162
+ ? { oldPath: unquote(rest), newPath: null, prefixed: false }
163
+ : { oldPath: unquote(rest.slice(0, split)), newPath: unquote(rest.slice(split + 1)), prefixed: false };
164
+ }
165
+
166
+ function emptySection(firstLine: number, git: boolean): Section {
167
+ return {
168
+ oldPath: null,
169
+ newPath: null,
170
+ git,
171
+ created: false,
172
+ deleted: false,
173
+ renamed: false,
174
+ copied: false,
175
+ binary: false,
176
+ firstLine,
177
+ lastLine: firstLine,
178
+ added: 0,
179
+ removed: 0,
180
+ hunks: [],
181
+ named: false,
182
+ prefixed: false
183
+ };
184
+ }
185
+
186
+ function statusOf(section: Section): PatchFileStatus {
187
+ if (section.binary) {
188
+ return 'binary';
189
+ }
190
+ if (section.created || (!section.git && section.oldPath === null && section.newPath !== null)) {
191
+ return 'added';
192
+ }
193
+ if (section.deleted || (!section.git && section.newPath === null && section.oldPath !== null)) {
194
+ return 'deleted';
195
+ }
196
+ if (section.renamed) {
197
+ return 'renamed';
198
+ }
199
+ return section.copied ? 'copied' : 'modified';
200
+ }
201
+
202
+ function linesOf(text: string, limits: Required<PatchLimits>): { lines: string[]; truncated: boolean } {
203
+ const lines: string[] = [];
204
+ let start = 0;
205
+ while (lines.length < limits.maxLines && start <= limits.maxCharacters) {
206
+ const end = text.indexOf('\n', start);
207
+ if (end === -1) {
208
+ lines.push(text.slice(start).replace(/\r$/, ''));
209
+ return { lines, truncated: false };
210
+ }
211
+ lines.push(text.slice(start, end).replace(/\r$/, ''));
212
+ start = end + 1;
213
+ }
214
+ // Stopping right after a final newline leaves nothing unread.
215
+ return { lines, truncated: start < text.length };
216
+ }
217
+
218
+ /*
219
+ * What every line of a patch is, with the file numbers of its lines and the files it touches. It reads
220
+ * git patches and plain `diff -u` output, tolerates a text that was edited so its hunks no longer add up
221
+ * (the hunk is marked inconsistent and the reading goes on) and never throws. Past the limits it stops
222
+ * and says so in `truncated`.
223
+ */
224
+ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayout {
225
+ const { lines: source, truncated } = linesOf(text, { ...DEFAULT_PATCH_LIMITS, ...limits });
226
+ const lines: PatchLayoutLine[] = [];
227
+ const sections: Section[] = [];
228
+ let section: Section | undefined;
229
+ let hunk: OpenHunk | undefined;
230
+ let binaryData = false;
231
+ const seal = (): void => {
232
+ if (hunk !== undefined && (hunk.oldLeft > 0 || hunk.newLeft > 0)) {
233
+ hunk.layout.consistent = false;
234
+ }
235
+ hunk = undefined;
236
+ };
237
+ const open = (line: number, git: boolean): Section => {
238
+ seal();
239
+ binaryData = false;
240
+ section = emptySection(line, git);
241
+ sections.push(section);
242
+ return section;
243
+ };
244
+ // A removed line can read `-- x`, so `---` starts a file only with `+++` under it.
245
+ const startsFile = (index: number): boolean => source[index]!.startsWith('--- ') && (source[index + 1]?.startsWith('+++ ') ?? false);
246
+ const push = (line: PatchLayoutLine, index: number): void => {
247
+ lines.push(line);
248
+ if (section !== undefined) {
249
+ section.lastLine = index;
250
+ }
251
+ };
252
+ for (let index = 0; index < source.length; index++) {
253
+ const line = source[index]!;
254
+ const remaining = hunk !== undefined && (hunk.oldLeft > 0 || hunk.newLeft > 0);
255
+ const gitStart = line.startsWith('diff --git ');
256
+ const header = line.startsWith('@@') ? parseHunkHeader(line) : undefined;
257
+ const fileStart = startsFile(index) && (!remaining || source[index + 2]?.startsWith('@@') === true);
258
+ if (remaining && !gitStart && header === undefined && !fileStart) {
259
+ if (isContent(line) || line === '' || line.startsWith('\\')) {
260
+ push(contentLine(line, hunk!, section!), index);
261
+ } else {
262
+ hunk!.layout.consistent = false;
263
+ push(OTHER, index);
264
+ }
265
+ continue;
266
+ }
267
+ if (gitStart) {
268
+ const created = open(index, true);
269
+ ({ oldPath: created.oldPath, newPath: created.newPath, prefixed: created.prefixed } = gitPaths(line.slice('diff --git '.length)));
270
+ push(FILE_HEADER, index);
271
+ continue;
272
+ }
273
+ if (fileStart) {
274
+ const reuse = section !== undefined && section.git && !section.named && section.hunks.length === 0;
275
+ const target = reuse ? section! : open(index, false);
276
+ seal();
277
+ nameSides(target, line, source[index + 1]!);
278
+ push(FILE_HEADER, index);
279
+ push(FILE_HEADER, index + 1);
280
+ index++;
281
+ continue;
282
+ }
283
+ if (header !== undefined) {
284
+ seal();
285
+ const target = section ?? open(index, false);
286
+ const layout: Mutable<PatchHunkLayout> = { line: index, ...header, consistent: true };
287
+ target.hunks.push(layout);
288
+ hunk = { layout, oldLeft: header.oldCount, newLeft: header.newCount, oldNumber: header.oldStart, newNumber: header.newStart };
289
+ push(HUNK_HEADER, index);
290
+ continue;
291
+ }
292
+ if (section === undefined) {
293
+ push(OTHER, index);
294
+ } else if (hunk !== undefined && line.startsWith('\\')) {
295
+ push(NO_NEWLINE, index);
296
+ } else if (hunk !== undefined && isContent(line) && line !== '-- ') {
297
+ // The counts ran out, so a line a person typed below the hunk extends it. The `-- ` of a mail signature does not.
298
+ hunk.layout.consistent = false;
299
+ push(contentLine(line, hunk, section), index);
300
+ } else if (hunk === undefined && section.git && GIT_HEADERS.test(line)) {
301
+ readGitHeader(section, line);
302
+ push(FILE_HEADER, index);
303
+ } else if (/^Binary files .* differ$/.test(line) || line === 'GIT binary patch') {
304
+ section.binary = true;
305
+ binaryData = line === 'GIT binary patch';
306
+ push(BINARY, index);
307
+ } else if (binaryData && line !== '') {
308
+ push(BINARY, index);
309
+ } else if (line === '') {
310
+ lines.push(OTHER);
311
+ } else {
312
+ push(OTHER, index);
313
+ }
314
+ }
315
+ seal();
316
+ return {
317
+ lines,
318
+ truncated,
319
+ files: sections.map((entry) => ({
320
+ oldPath: entry.oldPath,
321
+ newPath: entry.newPath,
322
+ status: statusOf(entry),
323
+ firstLine: entry.firstLine,
324
+ lastLine: entry.lastLine,
325
+ added: entry.added,
326
+ removed: entry.removed,
327
+ hunks: entry.hunks
328
+ }))
329
+ };
330
+ }
331
+
332
+ function isContent(line: string): boolean {
333
+ return line.startsWith('+') || line.startsWith('-') || line.startsWith(' ');
334
+ }
335
+
336
+ /* A line under a hunk header; an empty line is a context line whose space an editor trimmed. */
337
+ function contentLine(line: string, hunk: OpenHunk, section: Section): PatchLayoutLine {
338
+ if (line.startsWith('\\')) {
339
+ return NO_NEWLINE;
340
+ }
341
+ const kind = line.startsWith('+') ? 'added' : line.startsWith('-') ? 'removed' : 'context';
342
+ const takesOld = kind !== 'added';
343
+ const takesNew = kind !== 'removed';
344
+ if ((takesOld && hunk.oldLeft === 0) || (takesNew && hunk.newLeft === 0)) {
345
+ hunk.layout.consistent = false;
346
+ }
347
+ if (kind === 'added') {
348
+ section.added++;
349
+ } else if (kind === 'removed') {
350
+ section.removed++;
351
+ }
352
+ const oldNumber = takesOld ? hunk.oldNumber++ : null;
353
+ const newNumber = takesNew ? hunk.newNumber++ : null;
354
+ hunk.oldLeft = takesOld ? Math.max(0, hunk.oldLeft - 1) : hunk.oldLeft;
355
+ hunk.newLeft = takesNew ? Math.max(0, hunk.newLeft - 1) : hunk.newLeft;
356
+ return { role: kind, old: oldNumber, new: newNumber };
357
+ }
358
+
359
+ function nameSides(section: Section, oldLine: string, newLine: string): void {
360
+ const oldName = pathOf(oldLine.slice(4));
361
+ const newName = pathOf(newLine.slice(4));
362
+ const prefixed = section.git ? section.prefixed : (oldName?.startsWith('a/') ?? false) && (newName?.startsWith('b/') ?? false);
363
+ section.oldPath = prefixed ? stripPrefix(oldName, 'a/') : oldName;
364
+ section.newPath = prefixed ? stripPrefix(newName, 'b/') : newName;
365
+ section.created ||= oldName === null;
366
+ section.deleted ||= newName === null;
367
+ section.named = true;
368
+ }
369
+
370
+ function readGitHeader(section: Section, line: string): void {
371
+ if (line.startsWith('new file mode ')) {
372
+ section.created = true;
373
+ } else if (line.startsWith('deleted file mode ')) {
374
+ section.deleted = true;
375
+ } else if (line.startsWith('rename from ')) {
376
+ section.renamed = true;
377
+ section.oldPath = pathOf(line.slice('rename from '.length));
378
+ } else if (line.startsWith('rename to ')) {
379
+ section.renamed = true;
380
+ section.newPath = pathOf(line.slice('rename to '.length));
381
+ } else if (line.startsWith('copy from ')) {
382
+ section.copied = true;
383
+ section.oldPath = pathOf(line.slice('copy from '.length));
384
+ } else if (line.startsWith('copy to ')) {
385
+ section.copied = true;
386
+ section.newPath = pathOf(line.slice('copy to '.length));
387
+ }
388
+ }
package/src/peek.ts CHANGED
@@ -126,6 +126,7 @@ export class PeekFeature {
126
126
  ? files[0]!.places[0]!
127
127
  : (files.flatMap((file) => file.places).find((place) => !(place.location.uri === uri && place.line === position.line)) ?? files[0]!.places[0]!);
128
128
  this.line = position.line;
129
+ this.language.callHierarchy.close();
129
130
  this.language.popups.setState({ peek: { kind, container: null, files, active: first.id, collapsed: [], count: unique.length, preview: null } });
130
131
  this.select(first.id);
131
132
  // The editor draws a row once per id, so a peek opened over another needs an id of its own to get a container.
package/src/pick.ts CHANGED
@@ -12,6 +12,8 @@ export interface PickSpec {
12
12
  /* The row the list opens on; the first one without it. */
13
13
  readonly active?: string;
14
14
  accept(id: string): void;
15
+ /* A list can hand Tab back to its surrounding workflow after it closes. */
16
+ onTab?(backwards: boolean): void;
15
17
  /*
16
18
  * Asked before the list closes on a row. A reason it cannot be taken keeps the list open with the reason
17
19
  * under it, the way a rename keeps its input; null lets the list close and `accept` run.
@@ -154,9 +156,18 @@ export class PickFeature {
154
156
  this.activate(this.ids[this.ids.length - 1]!);
155
157
  return true;
156
158
  case 'Enter':
157
- case 'Tab':
158
159
  this.choose(this.active);
159
160
  return true;
161
+ case 'Tab': {
162
+ const spec = this.spec;
163
+ if (spec.onTab !== undefined) {
164
+ this.close();
165
+ spec.onTab(event.shiftKey);
166
+ } else {
167
+ this.choose(this.active);
168
+ }
169
+ return true;
170
+ }
160
171
  case 'Escape':
161
172
  this.close();
162
173
  return true;
package/src/popups.ts CHANGED
@@ -4,10 +4,12 @@ import type { Location } from '@adecore/lsp';
4
4
  import type { CodeAuthorship } from './code-authors.ts';
5
5
  import type { Problem } from './diagnostics-model.ts';
6
6
  import type { PeekFile, PeekSnippet } from './peek-model.ts';
7
+ import type { CallDirection, CallRow } from './call-hierarchy-model.ts';
7
8
  import type { ActionEntry } from './code-actions-model.ts';
8
9
  import type { SymbolEntry } from './symbol-picker-model.ts';
9
10
  import type { HoverText } from './hover-content.ts';
10
11
  import type { SignatureViewModel } from './signature-model.ts';
12
+ import type { ResourceOperationView } from './workspace-edit.ts';
11
13
 
12
14
  /* What a hover card shows for the character under the pointer. */
13
15
  export interface HoverView {
@@ -29,6 +31,8 @@ export interface HoverInfo {
29
31
  readonly word: string;
30
32
  /* How many other places use the symbol, once a server has counted them. */
31
33
  readonly references: number | null;
34
+ /* Only the text: no footer for the definition and the references, which a card made of someone else's words has none of. */
35
+ readonly plain?: boolean;
32
36
  }
33
37
 
34
38
  /* One line of the suggestions list. */
@@ -132,6 +136,8 @@ export interface RenameView {
132
136
  readonly error: string | null;
133
137
  readonly name: string;
134
138
  readonly files: readonly RenameFileView[];
139
+ /* The files the rename makes, moves or deletes, listed in the preview next to the lines. */
140
+ readonly operations?: readonly ResourceOperationView[];
135
141
  /* Names the host suggests from how the symbol is used. They fill the input when chosen and never apply by themselves. */
136
142
  readonly suggestions: readonly string[];
137
143
  }
@@ -150,6 +156,21 @@ export interface PeekView {
150
156
  readonly preview: (PeekSnippet & { readonly uri: string }) | null;
151
157
  }
152
158
 
159
+ /* The callers of a symbol, or the symbols it calls, as a tree between the lines of the file with the code of the active call. */
160
+ export interface CallHierarchyView {
161
+ readonly direction: CallDirection;
162
+ /* The row the editor draws it in, which the editor makes again when it scrolls back into view. */
163
+ readonly container: HTMLElement | null;
164
+ /* The symbol the hierarchy was opened on. */
165
+ readonly name: string;
166
+ readonly rows: readonly CallRow[];
167
+ readonly active: string;
168
+ /* The call site of the active row that is shown, from zero, among `sites`. */
169
+ readonly site: number;
170
+ readonly sites: number;
171
+ readonly preview: (PeekSnippet & { readonly uri: string }) | null;
172
+ }
173
+
153
174
  /* The picker that jumps to a symbol of the file, over the symbols the servers know in it. */
154
175
  export interface SymbolsView {
155
176
  /* The file's name, for the placeholder. */
@@ -182,6 +203,8 @@ export interface PopupState {
182
203
  readonly pick: PickView | null;
183
204
  readonly rename: RenameView | null;
184
205
  readonly peek: PeekView | null;
206
+ /* Optional so a host that builds a `PopupState` literal from before the call hierarchy still fits; absent reads as closed. */
207
+ readonly callHierarchy?: CallHierarchyView | null;
185
208
  readonly symbols: SymbolsView | null;
186
209
  readonly menu: MenuView | null;
187
210
  readonly authors: AuthorsView | null;
@@ -197,6 +220,7 @@ export function createPopupStore(): PopupStore {
197
220
  pick: null,
198
221
  rename: null,
199
222
  peek: null,
223
+ callHierarchy: null,
200
224
  symbols: null,
201
225
  menu: null,
202
226
  authors: null
@@ -1,5 +1,6 @@
1
+ import { disposeSnippetTransforms } from './snippet-transform-queue.ts';
1
2
  import i18next, { type i18n } from 'i18next';
2
- import { fileUriToPath, type ApplyWorkspaceEditResult, type ContentChange, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
3
+ import { fileUriToPath, pathToFileUri, type ApplyWorkspaceEditResult, type ContentChange, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
3
4
  import type { Editor, EditorPosition } from '@adecore/editor';
4
5
  import { NavigationHistory, type Place } from './navigation-history.ts';
5
6
  import { ProjectProblems } from './project-problems.ts';
@@ -73,6 +74,17 @@ interface Holder {
73
74
  languageId: string;
74
75
  }
75
76
 
77
+ /* A file or folder for a file operation notification: a bare path is a file. */
78
+ export type FileOperationTarget = string | { readonly path: string; readonly directory?: boolean };
79
+
80
+ function filesOf(targets: readonly FileOperationTarget[]): Array<{ uri: string; directory: boolean }> {
81
+ return targets.map((target) => {
82
+ const { path, directory } =
83
+ typeof target === 'string' ? { path: target, directory: false } : { path: target.path, directory: target.directory === true };
84
+ return { uri: pathToFileUri(path), directory };
85
+ });
86
+ }
87
+
76
88
  /*
77
89
  * A shared language service and document ownership for the editors of a project. Two editors on the same
78
90
  * file hold one document, and only the first one sends changes, since both would report the same edit.
@@ -109,7 +121,35 @@ export class ProjectLanguage {
109
121
 
110
122
  /* Makes an edit of a language server: an open document takes it as one undo step and any other file gets an unsaved draft. */
111
123
  applyWorkspaceEdit(edit: WorkspaceEdit): Promise<ApplyWorkspaceEditResult> {
112
- return applyWorkspaceEdit(edit, { editorOf: (uri) => this.holders.get(uri)?.editors[0], files: this.files });
124
+ const { service } = this;
125
+ return applyWorkspaceEdit(edit, {
126
+ editorOf: (uri) => this.holders.get(uri)?.editors[0],
127
+ files: this.files,
128
+ ...(service.versionOf === undefined ? {} : { versionOf: (uri: string) => service.versionOf!(uri) })
129
+ });
130
+ }
131
+
132
+ /*
133
+ * What the servers want changed before files are made, for a host that makes them outside a language server's
134
+ * edit: apply the edit with `applyWorkspaceEdit`, then make the files. Null when no server has anything to say.
135
+ */
136
+ willCreateFiles(targets: readonly FileOperationTarget[]): Promise<WorkspaceEdit | null> {
137
+ return this.service.willCreateFiles?.(filesOf(targets)) ?? Promise.resolve(null);
138
+ }
139
+
140
+ /* Tells the servers that files were made, once they are. */
141
+ didCreateFiles(targets: readonly FileOperationTarget[]): Promise<void> {
142
+ return this.service.didCreateFiles?.(filesOf(targets)) ?? Promise.resolve();
143
+ }
144
+
145
+ /* What the servers want changed before files are deleted, such as the references to them; null when no server has anything to say. */
146
+ willDeleteFiles(targets: readonly FileOperationTarget[]): Promise<WorkspaceEdit | null> {
147
+ return this.service.willDeleteFiles?.(filesOf(targets)) ?? Promise.resolve(null);
148
+ }
149
+
150
+ /* Tells the servers that files were deleted, once they are. */
151
+ didDeleteFiles(targets: readonly FileOperationTarget[]): Promise<void> {
152
+ return this.service.didDeleteFiles?.(filesOf(targets)) ?? Promise.resolve();
113
153
  }
114
154
 
115
155
  /* The text of a file as it stands now: an open document's, else a draft's or the machine's; null when it is no text file. */
@@ -211,6 +251,7 @@ export class ProjectLanguage {
211
251
  return;
212
252
  }
213
253
  this.disposed = true;
254
+ disposeSnippetTransforms(this);
214
255
  for (const [uri, holder] of this.holders) {
215
256
  holder.sync.dispose();
216
257
  void holder.sync.ready
package/src/refresher.ts CHANGED
@@ -41,6 +41,12 @@ export class Refresher {
41
41
  });
42
42
  }
43
43
 
44
+ /* Drops the wait and the ask in flight, and keeps the refresher usable. */
45
+ cancel(): void {
46
+ this.timers.clear(this.timer);
47
+ this.controller?.abort();
48
+ }
49
+
44
50
  dispose(): void {
45
51
  this.disposed = true;
46
52
  this.timers.clear(this.timer);
@@ -1,7 +1,7 @@
1
1
  import type { EditorPosition, EditorRange } from '@adecore/editor';
2
2
  import type { Location, PrepareRenameResult, TextEdit, WorkspaceEdit } from '@adecore/lsp';
3
3
  import { isIdentifierCharacter } from './completion-model.ts';
4
- import { entriesOf } from './workspace-edit.ts';
4
+ import { textEntriesOf } from './workspace-edit.ts';
5
5
 
6
6
  /* The identifier the caret is in or touches; null where there is none. */
7
7
  export function wordRangeAt(lineText: string, position: EditorPosition): EditorRange | null {
@@ -115,14 +115,10 @@ export function renameRowsOf(text: string, edits: readonly TextEdit[]): RenameRo
115
115
  });
116
116
  }
117
117
 
118
- /* Resource renames are omitted; create/delete operations refuse the text-only preview with null. */
119
- export async function renamePreviewOf(edit: WorkspaceEdit, textOf: (uri: string) => Promise<string | null>): Promise<RenameFile[] | null> {
120
- const entries = entriesOf(edit);
121
- if (entries === null) {
122
- return null;
123
- }
118
+ /* The lines of the files an edit edits; the files it makes, moves or deletes are listed by `resourceOperationsOf`. */
119
+ export async function renamePreviewOf(edit: WorkspaceEdit, textOf: (uri: string) => Promise<string | null>): Promise<RenameFile[]> {
124
120
  const files: RenameFile[] = [];
125
- for (const [uri, list] of entries) {
121
+ for (const [uri, list] of textEntriesOf(edit)) {
126
122
  const text = await textOf(uri);
127
123
  files.push({ uri, rows: text === null ? [] : renameRowsOf(text, list.flat()) });
128
124
  }
package/src/rename.ts CHANGED
@@ -5,6 +5,7 @@ import { lineTextOf } from './line-text.ts';
5
5
  import type { RenameView } from './popups.ts';
6
6
  import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt, type RenameTarget } from './rename-model.ts';
7
7
  import { isShortcut } from './shortcut-keys.ts';
8
+ import { resourceOperationsOf } from './workspace-edit.ts';
8
9
  import { messageOf } from './error-message.ts';
9
10
 
10
11
  const METHOD = 'textDocument/rename';
@@ -86,7 +87,7 @@ export class RenameFeature {
86
87
  }
87
88
  const session: Session = { target, original: editor.textInRange(target.range), edit: null, names: new AbortController() };
88
89
  this.session = session;
89
- this.publish({ phase: 'input', busy: false, name: target.placeholder, files: [], occurrences: null, suggestions: [] });
90
+ this.publish({ phase: 'input', busy: false, name: target.placeholder, files: [], operations: [], occurrences: null, suggestions: [] });
90
91
  void this.light(session).then((locations) => this.suggest(session, locations));
91
92
  }
92
93
 
@@ -121,12 +122,7 @@ export class RenameFeature {
121
122
  if (this.session !== session) {
122
123
  return;
123
124
  }
124
- if (files === null) {
125
- // An edit that renames files too has no list of lines to show, so it is applied or refused as a whole.
126
- await this.apply(session);
127
- return;
128
- }
129
- this.publish({ phase: 'preview', busy: false, name: next, files });
125
+ this.publish({ phase: 'preview', busy: false, name: next, files, operations: resourceOperationsOf(edit) });
130
126
  } catch (error) {
131
127
  if (this.session === session) {
132
128
  this.fail(error instanceof StaleResultError ? this.say('stale') : messageOf(error));
@@ -172,7 +168,7 @@ export class RenameFeature {
172
168
  private fail(message: string): void {
173
169
  if (this.session !== null) {
174
170
  this.session.edit = null;
175
- this.publish({ phase: 'input', busy: false, files: [], error: this.say('failed', { message }) });
171
+ this.publish({ phase: 'input', busy: false, files: [], operations: [], error: this.say('failed', { message }) });
176
172
  }
177
173
  }
178
174
 
@@ -271,6 +267,7 @@ export class RenameFeature {
271
267
  error: null,
272
268
  name: session.target.placeholder,
273
269
  files: [],
270
+ operations: [],
274
271
  suggestions: [],
275
272
  ...current,
276
273
  ...patch